1
0
Эх сурвалжийг харах

Multiple key/salt pairs support

DarthSim 6 жил өмнө
parent
commit
9114f28c75
5 өөрчлөгдсөн 77 нэмэгдсэн , 39 устгасан
  1. 44 26
      config.go
  2. 10 6
      crypt.go
  3. 16 2
      crypt_test.go
  4. 3 1
      docs/configuration.md
  5. 4 4
      processing_options_test.go

+ 44 - 26
config.go

@@ -2,11 +2,9 @@ package main
 
 import (
 	"bufio"
-	"bytes"
 	"encoding/hex"
 	"flag"
 	"fmt"
-	"io/ioutil"
 	"log"
 	"os"
 	"runtime"
@@ -45,17 +43,25 @@ func boolEnvConfig(b *bool, name string) {
 	}
 }
 
-func hexEnvConfig(b *[]byte, name string) {
+func hexEnvConfig(b *[]securityKey, name string) {
 	var err error
 
 	if env := os.Getenv(name); len(env) > 0 {
-		if *b, err = hex.DecodeString(env); err != nil {
-			log.Fatalf("%s expected to be hex-encoded string\n", name)
+		parts := strings.Split(env, ",")
+
+		keys := make([]securityKey, len(parts))
+
+		for i, part := range parts {
+			if keys[i], err = hex.DecodeString(part); err != nil {
+				log.Fatalf("%s expected to be hex-encoded strings. Invalid: %s\n", name, part)
+			}
 		}
+
+		*b = keys
 	}
 }
 
-func hexFileConfig(b *[]byte, filepath string) {
+func hexFileConfig(b *[]securityKey, filepath string) {
 	if len(filepath) == 0 {
 		return
 	}
@@ -65,20 +71,28 @@ func hexFileConfig(b *[]byte, filepath string) {
 		log.Fatalf("Can't open file %s\n", filepath)
 	}
 
-	src, err := ioutil.ReadAll(f)
-	if err != nil {
-		log.Fatalln(err)
-	}
+	keys := []securityKey{}
 
-	src = bytes.TrimSpace(src)
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		part := scanner.Text()
 
-	dst := make([]byte, hex.DecodedLen(len(src)))
-	n, err := hex.Decode(dst, src)
-	if err != nil {
-		log.Fatalf("%s expected to contain hex-encoded string\n", filepath)
+		if len(part) == 0 {
+			continue
+		}
+
+		if key, err := hex.DecodeString(part); err == nil {
+			keys = append(keys, key)
+		} else {
+			log.Fatalf("%s expected to contain hex-encoded strings. Invalid: %s\n", filepath, part)
+		}
 	}
 
-	*b = dst[:n]
+	if err := scanner.Err(); err != nil {
+		log.Fatalf("Failed to read file %s: %s", filepath, err)
+	}
+
+	*b = keys
 }
 
 func presetEnvConfig(p presets, name string) {
@@ -137,8 +151,8 @@ type config struct {
 	EnforceWebp         bool
 	EnableClientHints   bool
 
-	Key           []byte
-	Salt          []byte
+	Keys          []securityKey
+	Salts         []securityKey
 	AllowInsecure bool
 	SignatureSize int
 
@@ -237,12 +251,12 @@ func init() {
 	boolEnvConfig(&conf.EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
 	boolEnvConfig(&conf.EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS")
 
-	hexEnvConfig(&conf.Key, "IMGPROXY_KEY")
-	hexEnvConfig(&conf.Salt, "IMGPROXY_SALT")
+	hexEnvConfig(&conf.Keys, "IMGPROXY_KEY")
+	hexEnvConfig(&conf.Salts, "IMGPROXY_SALT")
 	intEnvConfig(&conf.SignatureSize, "IMGPROXY_SIGNATURE_SIZE")
 
-	hexFileConfig(&conf.Key, *keyPath)
-	hexFileConfig(&conf.Salt, *saltPath)
+	hexFileConfig(&conf.Keys, *keyPath)
+	hexFileConfig(&conf.Salts, *saltPath)
 
 	strEnvConfig(&conf.Secret, "IMGPROXY_SECRET")
 
@@ -283,14 +297,18 @@ func init() {
 	strEnvConfig(&conf.HoneybadgerKey, "IMGPROXY_HONEYBADGER_KEY")
 	strEnvConfig(&conf.HoneybadgerEnv, "IMGPROXY_HONEYBADGER_ENV")
 
-	if len(conf.Key) == 0 {
-		warning("Key is not defined, so signature checking is disabled")
+	if len(conf.Keys) != len(conf.Salts) {
+		log.Fatalf("Number of keys and number of salts should be equal. Keys: %d, salts: %d", len(conf.Keys), len(conf.Salts))
+	}
+	if len(conf.Keys) == 0 {
+		warning("No keys defined, so signature checking is disabled")
 		conf.AllowInsecure = true
 	}
-	if len(conf.Salt) == 0 {
-		warning("Salt is not defined, so signature checking is disabled")
+	if len(conf.Salts) == 0 {
+		warning("No salts defined, so signature checking is disabled")
 		conf.AllowInsecure = true
 	}
+
 	if conf.SignatureSize < 1 || conf.SignatureSize > 32 {
 		log.Fatalf("Signature size should be within 1 and 32, now - %d\n", conf.SignatureSize)
 	}

+ 10 - 6
crypt.go

@@ -12,22 +12,26 @@ var (
 	errInvalidTokenEncoding = errors.New("Invalid token encoding")
 )
 
+type securityKey []byte
+
 func validatePath(token, path string) error {
 	messageMAC, err := base64.RawURLEncoding.DecodeString(token)
 	if err != nil {
 		return errInvalidTokenEncoding
 	}
 
-	if !hmac.Equal(messageMAC, signatureFor(path)) {
-		return errInvalidToken
+	for i := 0; i < len(conf.Keys); i++ {
+		if hmac.Equal(messageMAC, signatureFor(path, i)) {
+			return nil
+		}
 	}
 
-	return nil
+	return errInvalidToken
 }
 
-func signatureFor(str string) []byte {
-	mac := hmac.New(sha256.New, conf.Key)
-	mac.Write(conf.Salt)
+func signatureFor(str string, pairInd int) []byte {
+	mac := hmac.New(sha256.New, conf.Keys[pairInd])
+	mac.Write(conf.Salts[pairInd])
 	mac.Write([]byte(str))
 	expectedMAC := mac.Sum(nil)
 	if conf.SignatureSize < 32 {

+ 16 - 2
crypt_test.go

@@ -12,8 +12,8 @@ type CryptTestSuite struct{ MainTestSuite }
 func (s *CryptTestSuite) SetupTest() {
 	s.MainTestSuite.SetupTest()
 
-	conf.Key = []byte("test-key")
-	conf.Salt = []byte("test-salt")
+	conf.Keys = []securityKey{securityKey("test-key")}
+	conf.Salts = []securityKey{securityKey("test-salt")}
 }
 
 func (s *CryptTestSuite) TestValidatePath() {
@@ -33,6 +33,20 @@ func (s *CryptTestSuite) TestValidatePathInvalid() {
 	assert.Error(s.T(), err)
 }
 
+func (s *CryptTestSuite) TestValidatePathMultiplePairs() {
+	conf.Keys = append(conf.Keys, securityKey("test-key2"))
+	conf.Salts = append(conf.Salts, securityKey("test-salt2"))
+
+	err := validatePath("dtLwhdnPPiu_epMl1LrzheLpvHas-4mwvY6L3Z8WwlY", "asd")
+	assert.Nil(s.T(), err)
+
+	err = validatePath("jbDffNPt1-XBgDccsaE-XJB9lx8JIJqdeYIZKgOqZpg", "asd")
+	assert.Nil(s.T(), err)
+
+	err = validatePath("dtLwhdnPPis", "asd")
+	assert.Error(s.T(), err)
+}
+
 func TestCrypt(t *testing.T) {
 	suite.Run(t, new(CryptTestSuite))
 }

+ 3 - 1
docs/configuration.md

@@ -10,7 +10,9 @@ imgproxy allows URLs to be signed with a key and salt. This feature is disabled
 * `IMGPROXY_SALT`: hex-encoded salt;
 * `IMGPROXY_SIGNATURE_SIZE`: number of bytes to use for signature before encoding to Base64. Default: 32;
 
-You can also specify paths to files with a hex-encoded key and salt (useful in a development environment):
+You can specify multiple key/salt pairs by dividing keys and salts with comma (`,`). imgproxy will check URL signatures with each pair. Useful when you need to change key/salt pair in your application with zero downtime.
+
+You can also specify paths to files with a hex-encoded keys and salts, one by line (useful in a development environment):
 
 ```bash
 $ imgproxy -keypath /path/to/file/with/key -saltpath /path/to/file/with/salt

+ 4 - 4
processing_options_test.go

@@ -533,8 +533,8 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
 }
 
 func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
-	conf.Key = []byte("test-key")
-	conf.Salt = []byte("test-salt")
+	conf.Keys = []securityKey{securityKey("test-key")}
+	conf.Salts = []securityKey{securityKey("test-salt")}
 	conf.AllowInsecure = false
 
 	req := s.getRequest("http://example.com/HcvNognEV1bW6f8zRqxNYuOkV0IUf1xloRb57CzbT4g/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
@@ -544,8 +544,8 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
 }
 
 func (s *ProcessingOptionsTestSuite) TestParsePathSignedInvalid() {
-	conf.Key = []byte("test-key")
-	conf.Salt = []byte("test-salt")
+	conf.Keys = []securityKey{securityKey("test-key")}
+	conf.Salts = []securityKey{securityKey("test-salt")}
 	conf.AllowInsecure = false
 
 	req := s.getRequest("http://example.com/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")