mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-01-08 10:45:04 +02:00
Multiple key/salt pairs support
This commit is contained in:
parent
a537e05b57
commit
9114f28c75
70
config.go
70
config.go
@ -2,11 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"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
|
var err error
|
||||||
|
|
||||||
if env := os.Getenv(name); len(env) > 0 {
|
if env := os.Getenv(name); len(env) > 0 {
|
||||||
if *b, err = hex.DecodeString(env); err != nil {
|
parts := strings.Split(env, ",")
|
||||||
log.Fatalf("%s expected to be hex-encoded string\n", name)
|
|
||||||
|
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 {
|
if len(filepath) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -65,20 +71,28 @@ func hexFileConfig(b *[]byte, filepath string) {
|
|||||||
log.Fatalf("Can't open file %s\n", filepath)
|
log.Fatalf("Can't open file %s\n", filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
src, err := ioutil.ReadAll(f)
|
keys := []securityKey{}
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
part := scanner.Text()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
src = bytes.TrimSpace(src)
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatalf("Failed to read file %s: %s", filepath, err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*b = dst[:n]
|
*b = keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func presetEnvConfig(p presets, name string) {
|
func presetEnvConfig(p presets, name string) {
|
||||||
@ -137,8 +151,8 @@ type config struct {
|
|||||||
EnforceWebp bool
|
EnforceWebp bool
|
||||||
EnableClientHints bool
|
EnableClientHints bool
|
||||||
|
|
||||||
Key []byte
|
Keys []securityKey
|
||||||
Salt []byte
|
Salts []securityKey
|
||||||
AllowInsecure bool
|
AllowInsecure bool
|
||||||
SignatureSize int
|
SignatureSize int
|
||||||
|
|
||||||
@ -237,12 +251,12 @@ func init() {
|
|||||||
boolEnvConfig(&conf.EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
|
boolEnvConfig(&conf.EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
|
||||||
boolEnvConfig(&conf.EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS")
|
boolEnvConfig(&conf.EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS")
|
||||||
|
|
||||||
hexEnvConfig(&conf.Key, "IMGPROXY_KEY")
|
hexEnvConfig(&conf.Keys, "IMGPROXY_KEY")
|
||||||
hexEnvConfig(&conf.Salt, "IMGPROXY_SALT")
|
hexEnvConfig(&conf.Salts, "IMGPROXY_SALT")
|
||||||
intEnvConfig(&conf.SignatureSize, "IMGPROXY_SIGNATURE_SIZE")
|
intEnvConfig(&conf.SignatureSize, "IMGPROXY_SIGNATURE_SIZE")
|
||||||
|
|
||||||
hexFileConfig(&conf.Key, *keyPath)
|
hexFileConfig(&conf.Keys, *keyPath)
|
||||||
hexFileConfig(&conf.Salt, *saltPath)
|
hexFileConfig(&conf.Salts, *saltPath)
|
||||||
|
|
||||||
strEnvConfig(&conf.Secret, "IMGPROXY_SECRET")
|
strEnvConfig(&conf.Secret, "IMGPROXY_SECRET")
|
||||||
|
|
||||||
@ -283,14 +297,18 @@ func init() {
|
|||||||
strEnvConfig(&conf.HoneybadgerKey, "IMGPROXY_HONEYBADGER_KEY")
|
strEnvConfig(&conf.HoneybadgerKey, "IMGPROXY_HONEYBADGER_KEY")
|
||||||
strEnvConfig(&conf.HoneybadgerEnv, "IMGPROXY_HONEYBADGER_ENV")
|
strEnvConfig(&conf.HoneybadgerEnv, "IMGPROXY_HONEYBADGER_ENV")
|
||||||
|
|
||||||
if len(conf.Key) == 0 {
|
if len(conf.Keys) != len(conf.Salts) {
|
||||||
warning("Key is not defined, so signature checking is disabled")
|
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
|
conf.AllowInsecure = true
|
||||||
}
|
}
|
||||||
if len(conf.Salt) == 0 {
|
if len(conf.Salts) == 0 {
|
||||||
warning("Salt is not defined, so signature checking is disabled")
|
warning("No salts defined, so signature checking is disabled")
|
||||||
conf.AllowInsecure = true
|
conf.AllowInsecure = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.SignatureSize < 1 || conf.SignatureSize > 32 {
|
if conf.SignatureSize < 1 || conf.SignatureSize > 32 {
|
||||||
log.Fatalf("Signature size should be within 1 and 32, now - %d\n", conf.SignatureSize)
|
log.Fatalf("Signature size should be within 1 and 32, now - %d\n", conf.SignatureSize)
|
||||||
}
|
}
|
||||||
|
16
crypt.go
16
crypt.go
@ -12,22 +12,26 @@ var (
|
|||||||
errInvalidTokenEncoding = errors.New("Invalid token encoding")
|
errInvalidTokenEncoding = errors.New("Invalid token encoding")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type securityKey []byte
|
||||||
|
|
||||||
func validatePath(token, path string) error {
|
func validatePath(token, path string) error {
|
||||||
messageMAC, err := base64.RawURLEncoding.DecodeString(token)
|
messageMAC, err := base64.RawURLEncoding.DecodeString(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errInvalidTokenEncoding
|
return errInvalidTokenEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hmac.Equal(messageMAC, signatureFor(path)) {
|
for i := 0; i < len(conf.Keys); i++ {
|
||||||
return errInvalidToken
|
if hmac.Equal(messageMAC, signatureFor(path, i)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return errInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func signatureFor(str string) []byte {
|
func signatureFor(str string, pairInd int) []byte {
|
||||||
mac := hmac.New(sha256.New, conf.Key)
|
mac := hmac.New(sha256.New, conf.Keys[pairInd])
|
||||||
mac.Write(conf.Salt)
|
mac.Write(conf.Salts[pairInd])
|
||||||
mac.Write([]byte(str))
|
mac.Write([]byte(str))
|
||||||
expectedMAC := mac.Sum(nil)
|
expectedMAC := mac.Sum(nil)
|
||||||
if conf.SignatureSize < 32 {
|
if conf.SignatureSize < 32 {
|
||||||
|
@ -12,8 +12,8 @@ type CryptTestSuite struct{ MainTestSuite }
|
|||||||
func (s *CryptTestSuite) SetupTest() {
|
func (s *CryptTestSuite) SetupTest() {
|
||||||
s.MainTestSuite.SetupTest()
|
s.MainTestSuite.SetupTest()
|
||||||
|
|
||||||
conf.Key = []byte("test-key")
|
conf.Keys = []securityKey{securityKey("test-key")}
|
||||||
conf.Salt = []byte("test-salt")
|
conf.Salts = []securityKey{securityKey("test-salt")}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CryptTestSuite) TestValidatePath() {
|
func (s *CryptTestSuite) TestValidatePath() {
|
||||||
@ -33,6 +33,20 @@ func (s *CryptTestSuite) TestValidatePathInvalid() {
|
|||||||
assert.Error(s.T(), err)
|
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) {
|
func TestCrypt(t *testing.T) {
|
||||||
suite.Run(t, new(CryptTestSuite))
|
suite.Run(t, new(CryptTestSuite))
|
||||||
}
|
}
|
||||||
|
@ -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_SALT`: hex-encoded salt;
|
||||||
* `IMGPROXY_SIGNATURE_SIZE`: number of bytes to use for signature before encoding to Base64. Default: 32;
|
* `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
|
```bash
|
||||||
$ imgproxy -keypath /path/to/file/with/key -saltpath /path/to/file/with/salt
|
$ imgproxy -keypath /path/to/file/with/key -saltpath /path/to/file/with/salt
|
||||||
|
@ -533,8 +533,8 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
|
func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
|
||||||
conf.Key = []byte("test-key")
|
conf.Keys = []securityKey{securityKey("test-key")}
|
||||||
conf.Salt = []byte("test-salt")
|
conf.Salts = []securityKey{securityKey("test-salt")}
|
||||||
conf.AllowInsecure = false
|
conf.AllowInsecure = false
|
||||||
|
|
||||||
req := s.getRequest("http://example.com/HcvNognEV1bW6f8zRqxNYuOkV0IUf1xloRb57CzbT4g/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
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() {
|
func (s *ProcessingOptionsTestSuite) TestParsePathSignedInvalid() {
|
||||||
conf.Key = []byte("test-key")
|
conf.Keys = []securityKey{securityKey("test-key")}
|
||||||
conf.Salt = []byte("test-salt")
|
conf.Salts = []securityKey{securityKey("test-salt")}
|
||||||
conf.AllowInsecure = false
|
conf.AllowInsecure = false
|
||||||
|
|
||||||
req := s.getRequest("http://example.com/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
req := s.getRequest("http://example.com/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png")
|
||||||
|
Loading…
Reference in New Issue
Block a user