diff --git a/backend/crypt/cipher.go b/backend/crypt/cipher.go index d05a93924..95db1e5c2 100644 --- a/backend/crypt/cipher.go +++ b/backend/crypt/cipher.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "path" "strconv" "strings" "sync" @@ -487,12 +488,10 @@ func (c *Cipher) deobfuscateSegment(ciphertext string) (string, error) { // encryptFileName encrypts a file path // -// 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. +// If stripVersion is set, a version string (as used by --b2-versions) +// on the last segment is kept in plain text. Set it for file names +// only: directory names must encrypt identically whether passed alone +// or as the parent of a file name. func (c *Cipher) encryptFileName(in string, stripVersion bool) string { segments := strings.Split(in, "/") for i := range segments { @@ -540,6 +539,9 @@ func (c *Cipher) EncryptFileName(in string) string { } // EncryptDirName encrypts a directory path +// +// Unlike EncryptFileName, a version string on the last segment is +// encrypted along with the rest of the name. func (c *Cipher) EncryptDirName(in string) string { if c.mode == NameEncryptionOff || !c.dirNameEncrypt { return in @@ -614,11 +616,27 @@ func (c *Cipher) DecryptFileName(in string) (string, error) { } // DecryptDirName decrypts a directory path +// +// Unlike DecryptFileName, a version string on the last segment is +// expected to be part of the encrypted name. Directory names created +// by rclone before v1.76 had a version-like suffix left in plain +// text; these are still decrypted so that they appear in listings, +// but they can't be opened or removed until renamed on the +// underlying remote. func (c *Cipher) DecryptDirName(in string) (string, error) { if c.mode == NameEncryptionOff || !c.dirNameEncrypt { return in, nil } - return c.decryptFileName(in, false) + out, err := c.decryptFileName(in, false) + if err != nil && version.Match(path.Base(in)) { + var legacyErr error + out, legacyErr = c.decryptFileName(in, true) + if legacyErr == nil { + fs.Logf(nil, "crypt: directory %q has a legacy encrypted name - rename %q on the underlying remote to %q to make it accessible", out, in, c.encryptFileName(out, false)) + return out, nil + } + } + return out, err } // NameEncryptionMode returns the encryption mode in use for names diff --git a/backend/crypt/cipher_test.go b/backend/crypt/cipher_test.go index 217b85447..578a3a5c7 100644 --- a/backend/crypt/cipher_test.go +++ b/backend/crypt/cipher_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "path" "strings" "testing" @@ -684,23 +685,38 @@ 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 +// directory encrypted as the parent of a file name, whereas file names +// keep the version string in plain text 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) - } + enc, err := NewNameEncoding("base32") + require.NoError(t, err) + for _, mode := range []NameEncryptionMode{NameEncryptionStandard, NameEncryptionObfuscated} { + c, err := newCipher(mode, "", "", true, enc) + require.NoError(t, err) + what := fmt.Sprintf("Testing mode=%v", 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, path.Dir(encryptedFile), 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) + // Check a file with the same name keeps its version string in plain text + encryptedFile = c.EncryptFileName(dir) + assert.NotEqual(t, encryptedDir, encryptedFile, what) + assert.Contains(t, encryptedFile, "-v2001-02-03-040506-123", what) + decryptedFile, err := c.DecryptFileName(encryptedFile) + assert.NoError(t, err, what) + assert.Equal(t, dir, decryptedFile, what) + // Check a directory with a legacy encrypted name (the file form) still decrypts + decryptedDir, err = c.DecryptDirName(encryptedFile) + assert.NoError(t, err, what) + assert.Equal(t, dir, decryptedDir, what) + // but a name which is bad in both forms is still an error + _, err = c.DecryptDirName("!!!-v2001-02-03-040506-123") + assert.Error(t, err, what) } } diff --git a/docs/content/crypt.md b/docs/content/crypt.md index 63ef2b056..7e0e69805 100644 --- a/docs/content/crypt.md +++ b/docs/content/crypt.md @@ -813,6 +813,15 @@ This means that - filenames with the same name will encrypt the same - filenames which start the same won't have a common prefix +A version string of the form `-vYYYY-MM-DD-HHMMSS-NNN` on the end of a +file name (as added by `--b2-versions` / `--s3-versions`) is left in +plain text so that versioned files can be found. Directory names are +encrypted in full. Rclone before v1.76 left such a suffix in plain +text on directory names too, so a directory named like this created by +an older rclone will appear in listings with a warning but can't be +opened or removed until renamed on the underlying remote to the name +given in the warning. + This uses a 32 byte key (256 bits) and a 16 byte (128 bits) IV both of which are derived from the user password.