From 281a007b1a7867db5904ee09131e5809c0606f3d Mon Sep 17 00:00:00 2001 From: jladbrook Date: Thu, 20 Apr 2023 17:28:13 +0100 Subject: [PATCH] crypt: add suffix option to set a custom suffix for encrypted files - fixes #6392 --- backend/crypt/cipher.go | 53 +++++++++++++++++++++++------------- backend/crypt/cipher_test.go | 37 +++++++++++++++++-------- backend/crypt/crypt.go | 12 +++++++- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/backend/crypt/cipher.go b/backend/crypt/cipher.go index d457f3048..c6cdb47c6 100644 --- a/backend/crypt/cipher.go +++ b/backend/crypt/cipher.go @@ -38,7 +38,6 @@ const ( blockHeaderSize = secretbox.Overhead blockDataSize = 64 * 1024 blockSize = blockHeaderSize + blockDataSize - encryptedSuffix = ".bin" // when file name encryption is off we add this suffix to make sure the cloud provider doesn't process the file ) // Errors returned by cipher @@ -54,8 +53,9 @@ var ( ErrorEncryptedBadBlock = errors.New("failed to authenticate decrypted block - bad password?") ErrorBadBase32Encoding = errors.New("bad base32 filename encoding") ErrorFileClosed = errors.New("file already closed") - ErrorNotAnEncryptedFile = errors.New("not an encrypted file - no \"" + encryptedSuffix + "\" suffix") + ErrorNotAnEncryptedFile = errors.New("not an encrypted file - does not match suffix") ErrorBadSeek = errors.New("Seek beyond end of file") + ErrorSuffixMissingDot = errors.New("suffix config setting should include a '.'") defaultSalt = []byte{0xA8, 0x0D, 0xF4, 0x3A, 0x8F, 0xBD, 0x03, 0x08, 0xA7, 0xCA, 0xB8, 0x3E, 0x58, 0x1F, 0x86, 0xB1} obfuscQuoteRune = '!' ) @@ -170,25 +170,27 @@ func NewNameEncoding(s string) (enc fileNameEncoding, err error) { // Cipher defines an encoding and decoding cipher for the crypt backend type Cipher struct { - dataKey [32]byte // Key for secretbox - nameKey [32]byte // 16,24 or 32 bytes - nameTweak [nameCipherBlockSize]byte // used to tweak the name crypto - block gocipher.Block - mode NameEncryptionMode - fileNameEnc fileNameEncoding - buffers sync.Pool // encrypt/decrypt buffers - cryptoRand io.Reader // read crypto random numbers from here - dirNameEncrypt bool - passBadBlocks bool // if set passed bad blocks as zeroed blocks + dataKey [32]byte // Key for secretbox + nameKey [32]byte // 16,24 or 32 bytes + nameTweak [nameCipherBlockSize]byte // used to tweak the name crypto + block gocipher.Block + mode NameEncryptionMode + fileNameEnc fileNameEncoding + buffers sync.Pool // encrypt/decrypt buffers + cryptoRand io.Reader // read crypto random numbers from here + dirNameEncrypt bool + passBadBlocks bool // if set passed bad blocks as zeroed blocks + encryptedSuffix string } // newCipher initialises the cipher. If salt is "" then it uses a built in salt val func newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bool, enc fileNameEncoding) (*Cipher, error) { c := &Cipher{ - mode: mode, - fileNameEnc: enc, - cryptoRand: rand.Reader, - dirNameEncrypt: dirNameEncrypt, + mode: mode, + fileNameEnc: enc, + cryptoRand: rand.Reader, + dirNameEncrypt: dirNameEncrypt, + encryptedSuffix: ".bin", } c.buffers.New = func() interface{} { return new([blockSize]byte) @@ -200,6 +202,19 @@ func newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bo return c, nil } +// setEncryptedSuffix set suffix, or an empty string +func (c *Cipher) setEncryptedSuffix(suffix string) { + if strings.EqualFold(suffix, "none") { + c.encryptedSuffix = "" + return + } + if !strings.HasPrefix(suffix, ".") { + fs.Errorf(nil, "crypt: bad suffix: %v", ErrorSuffixMissingDot) + suffix = "." + suffix + } + c.encryptedSuffix = suffix +} + // Call to set bad block pass through func (c *Cipher) setPassBadBlocks(passBadBlocks bool) { c.passBadBlocks = passBadBlocks @@ -512,7 +527,7 @@ func (c *Cipher) encryptFileName(in string) string { // EncryptFileName encrypts a file path func (c *Cipher) EncryptFileName(in string) string { if c.mode == NameEncryptionOff { - return in + encryptedSuffix + return in + c.encryptedSuffix } return c.encryptFileName(in) } @@ -572,8 +587,8 @@ func (c *Cipher) decryptFileName(in string) (string, error) { // DecryptFileName decrypts a file path func (c *Cipher) DecryptFileName(in string) (string, error) { if c.mode == NameEncryptionOff { - remainingLength := len(in) - len(encryptedSuffix) - if remainingLength == 0 || !strings.HasSuffix(in, encryptedSuffix) { + remainingLength := len(in) - len(c.encryptedSuffix) + if remainingLength == 0 || !strings.HasSuffix(in, c.encryptedSuffix) { return "", ErrorNotAnEncryptedFile } decrypted := in[:remainingLength] diff --git a/backend/crypt/cipher_test.go b/backend/crypt/cipher_test.go index 1ed1125a2..559a6f549 100644 --- a/backend/crypt/cipher_test.go +++ b/backend/crypt/cipher_test.go @@ -405,6 +405,13 @@ func TestNonStandardEncryptFileName(t *testing.T) { // Off mode c, _ := newCipher(NameEncryptionOff, "", "", true, nil) assert.Equal(t, "1/12/123.bin", c.EncryptFileName("1/12/123")) + // Off mode with custom suffix + c, _ = newCipher(NameEncryptionOff, "", "", true, nil) + c.setEncryptedSuffix(".jpg") + assert.Equal(t, "1/12/123.jpg", c.EncryptFileName("1/12/123")) + // Off mode with empty suffix + c.setEncryptedSuffix("none") + assert.Equal(t, "1/12/123", c.EncryptFileName("1/12/123")) // Obfuscation mode c, _ = newCipher(NameEncryptionObfuscated, "", "", true, nil) assert.Equal(t, "49.6/99.23/150.890/53.!!lipps", c.EncryptFileName("1/12/123/!hello")) @@ -483,21 +490,27 @@ func TestNonStandardDecryptFileName(t *testing.T) { in string expected string expectedErr error + customSuffix string }{ - {NameEncryptionOff, true, "1/12/123.bin", "1/12/123", nil}, - {NameEncryptionOff, true, "1/12/123.bix", "", ErrorNotAnEncryptedFile}, - {NameEncryptionOff, true, ".bin", "", ErrorNotAnEncryptedFile}, - {NameEncryptionOff, true, "1/12/123-v2001-02-03-040506-123.bin", "1/12/123-v2001-02-03-040506-123", nil}, - {NameEncryptionOff, true, "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.bin", "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123", nil}, - {NameEncryptionOff, true, "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.txt.bin", "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.txt", nil}, - {NameEncryptionObfuscated, true, "!.hello", "hello", nil}, - {NameEncryptionObfuscated, true, "hello", "", ErrorNotAnEncryptedFile}, - {NameEncryptionObfuscated, true, "161.\u00e4", "\u00a1", nil}, - {NameEncryptionObfuscated, true, "160.\u03c2", "\u03a0", nil}, - {NameEncryptionObfuscated, false, "1/12/123/53.!!lipps", "1/12/123/!hello", nil}, - {NameEncryptionObfuscated, false, "1/12/123/53-v2001-02-03-040506-123.!!lipps", "1/12/123/!hello-v2001-02-03-040506-123", nil}, + {NameEncryptionOff, true, "1/12/123.bin", "1/12/123", nil, ""}, + {NameEncryptionOff, true, "1/12/123.bix", "", ErrorNotAnEncryptedFile, ""}, + {NameEncryptionOff, true, ".bin", "", ErrorNotAnEncryptedFile, ""}, + {NameEncryptionOff, true, "1/12/123-v2001-02-03-040506-123.bin", "1/12/123-v2001-02-03-040506-123", nil, ""}, + {NameEncryptionOff, true, "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.bin", "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123", nil, ""}, + {NameEncryptionOff, true, "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.txt.bin", "1/12/123-v1970-01-01-010101-123-v2001-02-03-040506-123.txt", nil, ""}, + {NameEncryptionOff, true, "1/12/123.jpg", "1/12/123", nil, ".jpg"}, + {NameEncryptionOff, true, "1/12/123", "1/12/123", nil, "none"}, + {NameEncryptionObfuscated, true, "!.hello", "hello", nil, ""}, + {NameEncryptionObfuscated, true, "hello", "", ErrorNotAnEncryptedFile, ""}, + {NameEncryptionObfuscated, true, "161.\u00e4", "\u00a1", nil, ""}, + {NameEncryptionObfuscated, true, "160.\u03c2", "\u03a0", nil, ""}, + {NameEncryptionObfuscated, false, "1/12/123/53.!!lipps", "1/12/123/!hello", nil, ""}, + {NameEncryptionObfuscated, false, "1/12/123/53-v2001-02-03-040506-123.!!lipps", "1/12/123/!hello-v2001-02-03-040506-123", nil, ""}, } { c, _ := newCipher(test.mode, "", "", test.dirNameEncrypt, enc) + if test.customSuffix != "" { + c.setEncryptedSuffix(test.customSuffix) + } actual, actualErr := c.DecryptFileName(test.in) what := fmt.Sprintf("Testing %q (mode=%v)", test.in, test.mode) assert.Equal(t, test.expected, actual, what) diff --git a/backend/crypt/crypt.go b/backend/crypt/crypt.go index 641d6642f..65664c798 100644 --- a/backend/crypt/crypt.go +++ b/backend/crypt/crypt.go @@ -48,7 +48,7 @@ func init() { Help: "Very simple filename obfuscation.", }, { Value: "off", - Help: "Don't encrypt the file names.\nAdds a \".bin\" extension only.", + Help: "Don't encrypt the file names.\nAdds a \".bin\", or \"suffix\" extension only.", }, }, }, { @@ -151,6 +151,14 @@ length and if it's case sensitive.`, }, }, Advanced: true, + }, { + Name: "suffix", + Help: `If this is set it will override the default suffix of ".bin". + +Setting suffix to "none" will result in an empty suffix. This may be useful +when the path length is critical.`, + Default: ".bin", + Advanced: true, }}, }) } @@ -183,6 +191,7 @@ func newCipherForConfig(opt *Options) (*Cipher, error) { if err != nil { return nil, fmt.Errorf("failed to make cipher: %w", err) } + cipher.setEncryptedSuffix(opt.Suffix) cipher.setPassBadBlocks(opt.PassBadBlocks) return cipher, nil } @@ -274,6 +283,7 @@ type Options struct { ShowMapping bool `config:"show_mapping"` PassBadBlocks bool `config:"pass_bad_blocks"` FilenameEncoding string `config:"filename_encoding"` + Suffix string `config:"suffix"` } // Fs represents a wrapped fs.Fs