diff --git a/backend/crypt/cipher.go b/backend/crypt/cipher.go index 6f4c987f6..d05a93924 100644 --- a/backend/crypt/cipher.go +++ b/backend/crypt/cipher.go @@ -486,7 +486,14 @@ func (c *Cipher) deobfuscateSegment(ciphertext string) (string, error) { } // encryptFileName encrypts a file path -func (c *Cipher) encryptFileName(in string) string { +// +// If stripVersion is set then a version string on the last segment (as +// used by --b2-versions) is removed before encryption and put back in +// plain text afterwards. Only file leaf names are given version strings +// by the underlying backend, so it must not be set for directory names, +// which would otherwise encrypt differently from the same directory +// appearing as the parent of a file name. +func (c *Cipher) encryptFileName(in string, stripVersion bool) string { segments := strings.Split(in, "/") for i := range segments { // Skip directory name encryption if the user chose to @@ -499,7 +506,7 @@ func (c *Cipher) encryptFileName(in string) string { // of the file name gets encrypted/obfuscated hasVersion := false var t time.Time - if i == (len(segments)-1) && version.Match(segments[i]) { + if stripVersion && i == (len(segments)-1) && version.Match(segments[i]) { var s string t, s = version.Remove(segments[i]) // version.Remove can fail, in which case it returns segments[i] @@ -529,7 +536,7 @@ func (c *Cipher) EncryptFileName(in string) string { if c.mode == NameEncryptionOff { return in + c.encryptedSuffix } - return c.encryptFileName(in) + return c.encryptFileName(in, true) } // EncryptDirName encrypts a directory path @@ -537,11 +544,13 @@ func (c *Cipher) EncryptDirName(in string) string { if c.mode == NameEncryptionOff || !c.dirNameEncrypt { return in } - return c.encryptFileName(in) + return c.encryptFileName(in, false) } // decryptFileName decrypts a file path -func (c *Cipher) decryptFileName(in string) (string, error) { +// +// stripVersion is as for encryptFileName. +func (c *Cipher) decryptFileName(in string, stripVersion bool) (string, error) { segments := strings.Split(in, "/") for i := range segments { var err error @@ -555,7 +564,7 @@ func (c *Cipher) decryptFileName(in string) (string, error) { // of the file name gets decrypted/deobfuscated hasVersion := false var t time.Time - if i == (len(segments)-1) && version.Match(segments[i]) { + if stripVersion && i == (len(segments)-1) && version.Match(segments[i]) { var s string t, s = version.Remove(segments[i]) // version.Remove can fail, in which case it returns segments[i] @@ -601,7 +610,7 @@ func (c *Cipher) DecryptFileName(in string) (string, error) { // Leave the version string on, if it was there return decrypted, nil } - return c.decryptFileName(in) + return c.decryptFileName(in, true) } // DecryptDirName decrypts a directory path @@ -609,7 +618,7 @@ func (c *Cipher) DecryptDirName(in string) (string, error) { if c.mode == NameEncryptionOff || !c.dirNameEncrypt { return in, nil } - return c.decryptFileName(in) + return c.decryptFileName(in, false) } // NameEncryptionMode returns the encryption mode in use for names diff --git a/backend/crypt/cipher_test.go b/backend/crypt/cipher_test.go index fe2edc6f9..217b85447 100644 --- a/backend/crypt/cipher_test.go +++ b/backend/crypt/cipher_test.go @@ -682,6 +682,28 @@ func TestNonStandardDecryptDirName(t *testing.T) { } } +// Test directories whose name looks like it has a version string - +// these encrypt verbatim, so that EncryptDirName agrees with the same +// directory encrypted as the parent of a file name +func TestVersionedDirName(t *testing.T) { + const dir = "dir-v2001-02-03-040506-123" + for _, encoding := range []string{"base32", "base64", "base32768"} { + enc, _ := NewNameEncoding(encoding) + for _, mode := range []NameEncryptionMode{NameEncryptionStandard, NameEncryptionObfuscated} { + c, _ := newCipher(mode, "", "", true, enc) + what := fmt.Sprintf("Testing %q (mode=%v)", encoding, mode) + // Check EncryptDirName matches the parent of an encrypted file name + encryptedDir := c.EncryptDirName(dir) + encryptedFile := c.EncryptFileName(dir + "/file.txt") + assert.Equal(t, encryptedDir+"/", encryptedFile[:strings.LastIndex(encryptedFile, "/")+1], what) + // Check the encrypted directory name round trips OK + decryptedDir, err := c.DecryptDirName(encryptedDir) + assert.NoError(t, err, what) + assert.Equal(t, dir, decryptedDir, what) + } + } +} + func TestEncryptedSize(t *testing.T) { c, _ := newCipher(NameEncryptionStandard, "", "", true, nil) for _, test := range []struct {