forked from TrueCloudLab/rclone
crypt: add base64 and base32768 filename encoding options #5801
This commit is contained in:
parent
4c93378f0e
commit
c217145cae
4 changed files with 90 additions and 28 deletions
|
@ -7,6 +7,7 @@ import (
|
||||||
gocipher "crypto/cipher"
|
gocipher "crypto/cipher"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -16,6 +17,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/Max-Sum/base32768"
|
||||||
"github.com/rclone/rclone/backend/crypt/pkcs7"
|
"github.com/rclone/rclone/backend/crypt/pkcs7"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/accounting"
|
"github.com/rclone/rclone/fs/accounting"
|
||||||
|
@ -114,6 +116,57 @@ func (mode NameEncryptionMode) String() (out string) {
|
||||||
return out
|
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
|
// Cipher defines an encoding and decoding cipher for the crypt backend
|
||||||
type Cipher struct {
|
type Cipher struct {
|
||||||
dataKey [32]byte // Key for secretbox
|
dataKey [32]byte // Key for secretbox
|
||||||
|
@ -121,15 +174,17 @@ type Cipher struct {
|
||||||
nameTweak [nameCipherBlockSize]byte // used to tweak the name crypto
|
nameTweak [nameCipherBlockSize]byte // used to tweak the name crypto
|
||||||
block gocipher.Block
|
block gocipher.Block
|
||||||
mode NameEncryptionMode
|
mode NameEncryptionMode
|
||||||
|
fileNameEnc fileNameEncoding
|
||||||
buffers sync.Pool // encrypt/decrypt buffers
|
buffers sync.Pool // encrypt/decrypt buffers
|
||||||
cryptoRand io.Reader // read crypto random numbers from here
|
cryptoRand io.Reader // read crypto random numbers from here
|
||||||
dirNameEncrypt bool
|
dirNameEncrypt bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCipher initialises the cipher. If salt is "" then it uses a built in salt val
|
// 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{
|
c := &Cipher{
|
||||||
mode: mode,
|
mode: mode,
|
||||||
|
fileNameEnc: enc,
|
||||||
cryptoRand: rand.Reader,
|
cryptoRand: rand.Reader,
|
||||||
dirNameEncrypt: dirNameEncrypt,
|
dirNameEncrypt: dirNameEncrypt,
|
||||||
}
|
}
|
||||||
|
@ -187,30 +242,6 @@ func (c *Cipher) putBlock(buf []byte) {
|
||||||
c.buffers.Put(buf)
|
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
|
// encryptSegment encrypts a path segment
|
||||||
//
|
//
|
||||||
// This uses EME with AES
|
// This uses EME with AES
|
||||||
|
@ -231,7 +262,7 @@ func (c *Cipher) encryptSegment(plaintext string) string {
|
||||||
}
|
}
|
||||||
paddedPlaintext := pkcs7.Pad(nameCipherBlockSize, []byte(plaintext))
|
paddedPlaintext := pkcs7.Pad(nameCipherBlockSize, []byte(plaintext))
|
||||||
ciphertext := eme.Transform(c.block, c.nameTweak[:], paddedPlaintext, eme.DirectionEncrypt)
|
ciphertext := eme.Transform(c.block, c.nameTweak[:], paddedPlaintext, eme.DirectionEncrypt)
|
||||||
return encodeFileName(ciphertext)
|
return c.fileNameEnc.EncodeToString(ciphertext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryptSegment decrypts a path segment
|
// decryptSegment decrypts a path segment
|
||||||
|
@ -239,7 +270,7 @@ func (c *Cipher) decryptSegment(ciphertext string) (string, error) {
|
||||||
if ciphertext == "" {
|
if ciphertext == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
rawCiphertext, err := decodeFileName(ciphertext)
|
rawCiphertext, err := c.fileNameEnc.DecodeString(ciphertext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,29 @@ names, or for debugging purposes.`,
|
||||||
Help: "Encrypt file data.",
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to make cipher: %w", err)
|
return nil, fmt.Errorf("failed to make cipher: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -229,6 +256,7 @@ type Options struct {
|
||||||
Password2 string `config:"password2"`
|
Password2 string `config:"password2"`
|
||||||
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
||||||
ShowMapping bool `config:"show_mapping"`
|
ShowMapping bool `config:"show_mapping"`
|
||||||
|
FilenameEncoding string `config:"filename_encoding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fs represents a wrapped fs.Fs
|
// Fs represents a wrapped fs.Fs
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -10,6 +10,7 @@ require (
|
||||||
github.com/Azure/azure-storage-blob-go v0.14.0
|
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.17
|
github.com/Azure/go-autorest/autorest/adal v0.9.17
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c
|
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/Unknwon/goconfig v0.0.0-20200908083735-df7de6a44db8
|
||||||
github.com/a8m/tree v0.0.0-20210414114729-ce3525c5c2ef
|
github.com/a8m/tree v0.0.0-20210414114729-ce3525c5c2ef
|
||||||
github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3
|
github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3
|
||||||
|
|
2
go.sum
2
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/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/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/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.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 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
|
||||||
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||||
|
|
Loading…
Reference in a new issue