From 3fe2aaf96c5b945c0f469952313e1e0a45e719c3 Mon Sep 17 00:00:00 2001 From: Dominik Mydlil Date: Tue, 6 Apr 2021 21:45:34 +0200 Subject: [PATCH] crypt: support timestamped filenames from --b2-versions With the file version format standardized in lib/version, `crypt` can now treat the version strings separately from the encrypted/decrypted file names. This allows --b2-versions to work with `crypt`. Fixes #1627 Co-authored-by: Luc Ritchie --- backend/crypt/cipher.go | 58 ++++++++++++++++++++++++++++++++++-- backend/crypt/cipher_test.go | 12 ++++++++ docs/content/b2.md | 5 ---- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/backend/crypt/cipher.go b/backend/crypt/cipher.go index 77dc34643..470da07a0 100644 --- a/backend/crypt/cipher.go +++ b/backend/crypt/cipher.go @@ -12,12 +12,14 @@ import ( "strconv" "strings" "sync" + "time" "unicode/utf8" "github.com/pkg/errors" "github.com/rclone/rclone/backend/crypt/pkcs7" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/accounting" + "github.com/rclone/rclone/lib/version" "github.com/rfjakob/eme" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/scrypt" @@ -442,11 +444,32 @@ func (c *Cipher) encryptFileName(in string) string { if !c.dirNameEncrypt && i != (len(segments)-1) { continue } + + // Strip version string so that only the non-versioned part + // of the file name gets encrypted/obfuscated + hasVersion := false + var t time.Time + if 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] + if s != segments[i] { + segments[i] = s + hasVersion = true + } + } + if c.mode == NameEncryptionStandard { segments[i] = c.encryptSegment(segments[i]) } else { segments[i] = c.obfuscateSegment(segments[i]) } + + // Add back a version to the encrypted/obfuscated + // file name, if we stripped it off earlier + if hasVersion { + segments[i] = version.Add(segments[i], t) + } } return strings.Join(segments, "/") } @@ -477,6 +500,21 @@ func (c *Cipher) decryptFileName(in string) (string, error) { if !c.dirNameEncrypt && i != (len(segments)-1) { continue } + + // Strip version string so that only the non-versioned part + // of the file name gets decrypted/deobfuscated + hasVersion := false + var t time.Time + if 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] + if s != segments[i] { + segments[i] = s + hasVersion = true + } + } + if c.mode == NameEncryptionStandard { segments[i], err = c.decryptSegment(segments[i]) } else { @@ -486,6 +524,12 @@ func (c *Cipher) decryptFileName(in string) (string, error) { if err != nil { return "", err } + + // Add back a version to the decrypted/deobfuscated + // file name, if we stripped it off earlier + if hasVersion { + segments[i] = version.Add(segments[i], t) + } } return strings.Join(segments, "/"), nil } @@ -494,10 +538,18 @@ func (c *Cipher) decryptFileName(in string) (string, error) { func (c *Cipher) DecryptFileName(in string) (string, error) { if c.mode == NameEncryptionOff { remainingLength := len(in) - len(encryptedSuffix) - if remainingLength > 0 && strings.HasSuffix(in, encryptedSuffix) { - return in[:remainingLength], nil + if remainingLength == 0 || !strings.HasSuffix(in, encryptedSuffix) { + return "", ErrorNotAnEncryptedFile } - return "", ErrorNotAnEncryptedFile + decrypted := in[:remainingLength] + if version.Match(decrypted) { + _, unversioned := version.Remove(decrypted) + if unversioned == "" { + return "", ErrorNotAnEncryptedFile + } + } + // Leave the version string on, if it was there + return decrypted, nil } return c.decryptFileName(in) } diff --git a/backend/crypt/cipher_test.go b/backend/crypt/cipher_test.go index ff9bb23c5..dbe0e42be 100644 --- a/backend/crypt/cipher_test.go +++ b/backend/crypt/cipher_test.go @@ -160,22 +160,29 @@ func TestEncryptFileName(t *testing.T) { assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptFileName("1")) assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptFileName("1/12")) assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", c.EncryptFileName("1/12/123")) + assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s-v2001-02-03-040506-123", c.EncryptFileName("1-v2001-02-03-040506-123")) + assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng-v2001-02-03-040506-123", c.EncryptFileName("1/12-v2001-02-03-040506-123")) // Standard mode with directory name encryption off c, _ = newCipher(NameEncryptionStandard, "", "", false) assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptFileName("1")) assert.Equal(t, "1/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptFileName("1/12")) assert.Equal(t, "1/12/qgm4avr35m5loi1th53ato71v0", c.EncryptFileName("1/12/123")) + assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s-v2001-02-03-040506-123", c.EncryptFileName("1-v2001-02-03-040506-123")) + assert.Equal(t, "1/l42g6771hnv3an9cgc8cr2n1ng-v2001-02-03-040506-123", c.EncryptFileName("1/12-v2001-02-03-040506-123")) // Now off mode c, _ = newCipher(NameEncryptionOff, "", "", true) assert.Equal(t, "1/12/123.bin", c.EncryptFileName("1/12/123")) // Obfuscation mode c, _ = newCipher(NameEncryptionObfuscated, "", "", true) assert.Equal(t, "49.6/99.23/150.890/53.!!lipps", c.EncryptFileName("1/12/123/!hello")) + assert.Equal(t, "49.6/99.23/150.890/53-v2001-02-03-040506-123.!!lipps", c.EncryptFileName("1/12/123/!hello-v2001-02-03-040506-123")) + assert.Equal(t, "49.6/99.23/150.890/162.uryyB-v2001-02-03-040506-123.GKG", c.EncryptFileName("1/12/123/hello-v2001-02-03-040506-123.txt")) assert.Equal(t, "161.\u00e4", c.EncryptFileName("\u00a1")) assert.Equal(t, "160.\u03c2", c.EncryptFileName("\u03a0")) // Obfuscation mode with directory name encryption off c, _ = newCipher(NameEncryptionObfuscated, "", "", false) assert.Equal(t, "1/12/123/53.!!lipps", c.EncryptFileName("1/12/123/!hello")) + assert.Equal(t, "1/12/123/53-v2001-02-03-040506-123.!!lipps", c.EncryptFileName("1/12/123/!hello-v2001-02-03-040506-123")) assert.Equal(t, "161.\u00e4", c.EncryptFileName("\u00a1")) assert.Equal(t, "160.\u03c2", c.EncryptFileName("\u03a0")) } @@ -194,14 +201,19 @@ func TestDecryptFileName(t *testing.T) { {NameEncryptionStandard, true, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil}, {NameEncryptionStandard, true, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1/qgm4avr35m5loi1th53ato71v0", "", ErrorNotAMultipleOfBlocksize}, {NameEncryptionStandard, false, "1/12/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil}, + {NameEncryptionStandard, true, "p0e52nreeaj0a5ea7s64m4j72s-v2001-02-03-040506-123", "1-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}, {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) actual, actualErr := c.DecryptFileName(test.in) diff --git a/docs/content/b2.md b/docs/content/b2.md index 427935b1e..4967f7415 100644 --- a/docs/content/b2.md +++ b/docs/content/b2.md @@ -172,11 +172,6 @@ the file instead of hiding it. Old versions of files, where available, are visible using the `--b2-versions` flag. -**NB** Note that `--b2-versions` does not work with crypt at the -moment [#1627](https://github.com/rclone/rclone/issues/1627). Using -[--backup-dir](/docs/#backup-dir-dir) with rclone is the recommended -way of working around this. - If you wish to remove all the old versions then you can use the `rclone cleanup remote:bucket` command which will delete all the old versions of files, leaving the current ones intact. You can also