diff --git a/backend/crypt/cipher.go b/backend/crypt/cipher.go index 161ccc818..4a3556295 100644 --- a/backend/crypt/cipher.go +++ b/backend/crypt/cipher.go @@ -7,6 +7,7 @@ import ( gocipher "crypto/cipher" "crypto/rand" "encoding/base32" + "encoding/base64" "errors" "fmt" "io" @@ -16,6 +17,7 @@ import ( "time" "unicode/utf8" + "github.com/Max-Sum/base32768" "github.com/rclone/rclone/backend/crypt/pkcs7" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/accounting" @@ -114,6 +116,57 @@ func (mode NameEncryptionMode) String() (out string) { return out } +// fileNameEncoding are the encoding methods dealing with encrypted file names +type fileNameEncoding interface { + EncodeToString(src []byte) string + DecodeString(s string) ([]byte, error) +} + +// caseInsensitiveBase32Encoding defines a file name encoding +// using a modified version of standard base32 as described in +// RFC4648 +// +// The standard encoding is modified in two ways +// * it becomes lower case (no-one likes upper case filenames!) +// * we strip the padding character `=` +type caseInsensitiveBase32Encoding struct{} + +// EncodeToString encodes a strign using the modified version of +// base32 encoding. +func (caseInsensitiveBase32Encoding) EncodeToString(src []byte) string { + encoded := base32.HexEncoding.EncodeToString(src) + encoded = strings.TrimRight(encoded, "=") + return strings.ToLower(encoded) +} + +// DecodeString decodes a string as encoded by EncodeToString +func (caseInsensitiveBase32Encoding) DecodeString(s string) ([]byte, error) { + if strings.HasSuffix(s, "=") { + return nil, ErrorBadBase32Encoding + } + // First figure out how many padding characters to add + roundUpToMultipleOf8 := (len(s) + 7) &^ 7 + equals := roundUpToMultipleOf8 - len(s) + s = strings.ToUpper(s) + "========"[:equals] + return base32.HexEncoding.DecodeString(s) +} + +// NewNameEncoding creates a NameEncoding from a string +func NewNameEncoding(s string) (enc fileNameEncoding, err error) { + s = strings.ToLower(s) + switch s { + case "base32": + enc = caseInsensitiveBase32Encoding{} + case "base64": + enc = base64.RawURLEncoding + case "base32768": + enc = base32768.SafeEncoding + default: + err = fmt.Errorf("Unknown file name encoding mode %q", s) + } + return enc, err +} + // Cipher defines an encoding and decoding cipher for the crypt backend type Cipher struct { dataKey [32]byte // Key for secretbox @@ -121,15 +174,17 @@ type Cipher struct { 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 } // newCipher initialises the cipher. If salt is "" then it uses a built in salt val -func newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bool) (*Cipher, error) { +func newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bool, enc fileNameEncoding) (*Cipher, error) { c := &Cipher{ mode: mode, + fileNameEnc: enc, cryptoRand: rand.Reader, dirNameEncrypt: dirNameEncrypt, } @@ -187,30 +242,6 @@ func (c *Cipher) putBlock(buf []byte) { c.buffers.Put(buf) } -// encodeFileName encodes a filename using a modified version of -// standard base32 as described in RFC4648 -// -// The standard encoding is modified in two ways -// * it becomes lower case (no-one likes upper case filenames!) -// * we strip the padding character `=` -func encodeFileName(in []byte) string { - encoded := base32.HexEncoding.EncodeToString(in) - encoded = strings.TrimRight(encoded, "=") - return strings.ToLower(encoded) -} - -// decodeFileName decodes a filename as encoded by encodeFileName -func decodeFileName(in string) ([]byte, error) { - if strings.HasSuffix(in, "=") { - return nil, ErrorBadBase32Encoding - } - // First figure out how many padding characters to add - roundUpToMultipleOf8 := (len(in) + 7) &^ 7 - equals := roundUpToMultipleOf8 - len(in) - in = strings.ToUpper(in) + "========"[:equals] - return base32.HexEncoding.DecodeString(in) -} - // encryptSegment encrypts a path segment // // This uses EME with AES @@ -231,7 +262,7 @@ func (c *Cipher) encryptSegment(plaintext string) string { } paddedPlaintext := pkcs7.Pad(nameCipherBlockSize, []byte(plaintext)) ciphertext := eme.Transform(c.block, c.nameTweak[:], paddedPlaintext, eme.DirectionEncrypt) - return encodeFileName(ciphertext) + return c.fileNameEnc.EncodeToString(ciphertext) } // decryptSegment decrypts a path segment @@ -239,7 +270,7 @@ func (c *Cipher) decryptSegment(ciphertext string) (string, error) { if ciphertext == "" { return "", nil } - rawCiphertext, err := decodeFileName(ciphertext) + rawCiphertext, err := c.fileNameEnc.DecodeString(ciphertext) if err != nil { return "", err } diff --git a/backend/crypt/crypt.go b/backend/crypt/crypt.go index 56a3dcccd..002fbc8cc 100644 --- a/backend/crypt/crypt.go +++ b/backend/crypt/crypt.go @@ -116,6 +116,29 @@ names, or for debugging purposes.`, Help: "Encrypt file data.", }, }, + }, { + Name: "filename_encoding", + Help: `How to encode the encrypted filename to text string. + +This option could help with shortening the encrypted filename. The +suitable option would depend on the way your remote count the filename +length and if it's case sensitve.`, + Default: "base32", + Examples: []fs.OptionExample{ + { + Value: "base32", + Help: "Encode using base32. Suitable for all remote.", + }, + { + Value: "base64", + Help: "Encode using base64. Suitable for case sensitive remote.", + }, + { + Value: "base32768", + Help: "Encode using base32768. Suitable if your remote counts UTF-16 or\nUnicode codepoint instead of UTF-8 byte length. (Eg. Onedrive)", + }, + }, + Advanced: true, }}, }) } @@ -140,7 +163,11 @@ func newCipherForConfig(opt *Options) (*Cipher, error) { return nil, fmt.Errorf("failed to decrypt password2: %w", err) } } - cipher, err := newCipher(mode, password, salt, opt.DirectoryNameEncryption) + enc, err := NewNameEncoding(opt.FilenameEncoding) + if err != nil { + return nil, err + } + cipher, err := newCipher(mode, password, salt, opt.DirectoryNameEncryption, enc) if err != nil { return nil, fmt.Errorf("failed to make cipher: %w", err) } @@ -229,6 +256,7 @@ type Options struct { Password2 string `config:"password2"` ServerSideAcrossConfigs bool `config:"server_side_across_configs"` ShowMapping bool `config:"show_mapping"` + FilenameEncoding string `config:"filename_encoding"` } // Fs represents a wrapped fs.Fs diff --git a/go.mod b/go.mod index 2a584c617..6f51e2999 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/Azure/azure-storage-blob-go v0.14.0 github.com/Azure/go-autorest/autorest/adal v0.9.17 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c + github.com/Max-Sum/base32768 v0.0.0-20191205131208-7937843c71d5 // indirect github.com/Unknwon/goconfig v0.0.0-20200908083735-df7de6a44db8 github.com/a8m/tree v0.0.0-20210414114729-ce3525c5c2ef github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 diff --git a/go.sum b/go.sum index 7b3fad806..dc3beb78f 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzS github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= +github.com/Max-Sum/base32768 v0.0.0-20191205131208-7937843c71d5 h1:w/vNc+SQRYKGWBHeDrzvvNttHwZEbSAP0kmTdORl4OI= +github.com/Max-Sum/base32768 v0.0.0-20191205131208-7937843c71d5/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=