diff --git a/README.md b/README.md index ad293cc6..f41d66d3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Features: * [Configuration](#configuration) * [Encryption](#encryption) * [Authentication](#authentication) + * [Encrypt the configuration](#encrypt-the-configuration) * [RTSP proxy mode](#rtsp-proxy-mode) * [Publish a webcam](#publish-a-webcam) * [Publish a Raspberry Pi Camera](#publish-a-raspberry-pi-camera) @@ -217,6 +218,26 @@ paths: **WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials. +### Encrypt the configuration + +The configuration file can be entirely encrypted for security purposes. + +An online encryption tool is [available here](https://play.golang.org/p/rX29jwObNe4). + +The encryption procedure is the following: + +1. NaCL's `crypto_secretbox` function is applied to the content of the configuration. NaCL is a cryptographic library available for [C/C++](https://nacl.cr.yp.to/secretbox.html), [Go](https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox), [C#](https://github.com/somdoron/NaCl.net) and many other languages; + +2. The string is prefixed with the nonce; + +3. The string is encoded with base64. + +After performing the encryption, it's enough to put the base64-encoded result into the configuration file, and launch the server with the `RTSP_CONFKEY` variable: + +``` +RTSP_CONFKEY=mykey ./rtsp-simple-server +``` + ### RTSP proxy mode _rtsp-simple-server_ is also a RTSP proxy, that is usually deployed in one of these scenarios: diff --git a/go.mod b/go.mod index 0b71839f..dcaf1466 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/notedit/rtmp v0.0.2 github.com/stretchr/testify v1.6.1 + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index fb656256..de5f99c1 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,17 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/conf/conf.go b/internal/conf/conf.go index c57558e9..fff4332b 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -1,12 +1,15 @@ package conf import ( + "encoding/base64" "fmt" + "io/ioutil" "os" "time" "github.com/aler9/gortsplib" "github.com/aler9/gortsplib/pkg/headers" + "golang.org/x/crypto/nacl/secretbox" "gopkg.in/yaml.v2" "github.com/aler9/rtsp-simple-server/internal/confenv" @@ -23,6 +26,25 @@ const ( EncryptionStrict ) +func decrypt(key string, byts []byte) ([]byte, error) { + enc, err := base64.StdEncoding.DecodeString(string(byts)) + if err != nil { + return nil, err + } + + var secretKey [32]byte + copy(secretKey[:], key) + + var decryptNonce [24]byte + copy(decryptNonce[:], enc[:24]) + decrypted, ok := secretbox.Open(nil, enc[24:], &decryptNonce, &secretKey) + if !ok { + return nil, fmt.Errorf("decryption error") + } + + return decrypted, nil +} + // Conf is the main program configuration. type Conf struct { LogLevel string `yaml:"logLevel"` @@ -228,15 +250,21 @@ func Load(fpath string) (*Conf, bool, error) { } } - f, err := os.Open(fpath) + byts, err := ioutil.ReadFile(fpath) if err != nil { - return false, err + return true, err } - defer f.Close() - err = yaml.NewDecoder(f).Decode(conf) + if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { + byts, err = decrypt(key, byts) + if err != nil { + return true, err + } + } + + err = yaml.Unmarshal(byts, conf) if err != nil { - return false, err + return true, err } return true, nil diff --git a/internal/conf/pathconf.go b/internal/conf/path.go similarity index 90% rename from internal/conf/pathconf.go rename to internal/conf/path.go index c36b7ece..7d7e0152 100644 --- a/internal/conf/pathconf.go +++ b/internal/conf/path.go @@ -3,6 +3,7 @@ package conf import ( "encoding/json" "fmt" + "net" "net/url" "regexp" "strings" @@ -12,9 +13,56 @@ import ( "github.com/aler9/gortsplib/pkg/base" ) +const userPassSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,{,}" + var reUserPass = regexp.MustCompile(`^[a-zA-Z0-9!\$\(\)\*\+\.;<=>\[\]\^_\-\{\}]+$`) -const userPassSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,{,}" +var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/]+$`) + +func parseIPCidrList(in []string) ([]interface{}, error) { + if len(in) == 0 { + return nil, nil + } + + var ret []interface{} + for _, t := range in { + _, ipnet, err := net.ParseCIDR(t) + if err == nil { + ret = append(ret, ipnet) + continue + } + + ip := net.ParseIP(t) + if ip != nil { + ret = append(ret, ip) + continue + } + + return nil, fmt.Errorf("unable to parse ip/network '%s'", t) + } + return ret, nil +} + +// CheckPathName checks if a path name is valid. +func CheckPathName(name string) error { + if name == "" { + return fmt.Errorf("cannot be empty") + } + + if name[0] == '/' { + return fmt.Errorf("can't begin with a slash") + } + + if name[len(name)-1] == '/' { + return fmt.Errorf("can't end with a slash") + } + + if !rePathName.MatchString(name) { + return fmt.Errorf("can contain only alfanumeric characters, underscore, minus or slash") + } + + return nil +} // PathConf is a path configuration. type PathConf struct { diff --git a/internal/conf/utils.go b/internal/conf/utils.go deleted file mode 100644 index ce174b0a..00000000 --- a/internal/conf/utils.go +++ /dev/null @@ -1,54 +0,0 @@ -package conf - -import ( - "fmt" - "net" - "regexp" -) - -var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/]+$`) - -// CheckPathName check if a path name is valid. -func CheckPathName(name string) error { - if name == "" { - return fmt.Errorf("cannot be empty") - } - - if name[0] == '/' { - return fmt.Errorf("can't begin with a slash") - } - - if name[len(name)-1] == '/' { - return fmt.Errorf("can't end with a slash") - } - - if !rePathName.MatchString(name) { - return fmt.Errorf("can contain only alfanumeric characters, underscore, minus or slash") - } - - return nil -} - -func parseIPCidrList(in []string) ([]interface{}, error) { - if len(in) == 0 { - return nil, nil - } - - var ret []interface{} - for _, t := range in { - _, ipnet, err := net.ParseCIDR(t) - if err == nil { - ret = append(ret, ipnet) - continue - } - - ip := net.ParseIP(t) - if ip != nil { - ret = append(ret, ip) - continue - } - - return nil, fmt.Errorf("unable to parse ip/network '%s'", t) - } - return ret, nil -} diff --git a/main_test.go b/main_test.go index 2a2eb1fd..df0a86c1 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,9 @@ package main import ( + "crypto/rand" + "encoding/base64" + "io" "io/ioutil" "net" "os" @@ -13,6 +16,7 @@ import ( "github.com/aler9/gortsplib" "github.com/stretchr/testify/require" + "golang.org/x/crypto/nacl/secretbox" "github.com/aler9/rtsp-simple-server/internal/conf" ) @@ -245,6 +249,41 @@ func TestEnvironmentNoFile(t *testing.T) { }, pa) } +func TestEncryptedConf(t *testing.T) { + key := "testing123testin" + plaintext := ` +paths: + path1: + path2: +` + + encryptedConf := func() string { + var secretKey [32]byte + copy(secretKey[:], key[:]) + + var nonce [24]byte + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + panic(err) + } + + encrypted := secretbox.Seal(nonce[:], []byte(plaintext), &nonce, &secretKey) + return base64.StdEncoding.EncodeToString(encrypted) + }() + + os.Setenv("RTSP_CONFKEY", key) + defer os.Unsetenv("RTSP_CONFKEY") + + p, ok := testProgram(encryptedConf) + require.Equal(t, true, ok) + defer p.close() + + _, ok = p.conf.Paths["path1"] + require.Equal(t, true, ok) + + _, ok = p.conf.Paths["path2"] + require.Equal(t, true, ok) +} + var serverCert = []byte(`-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM