1054 lines · 27.8 KB · Go
RawDownload
package crypt import (	"bytes"	"context"	"crypto/aes"	gocipher "crypto/cipher"	"crypto/rand"	"encoding/base32"	"fmt"	"io"	"strconv"	"strings"	"sync"	"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/rfjakob/eme"	"golang.org/x/crypto/nacl/secretbox"	"golang.org/x/crypto/scrypt") // Constantsconst (	nameCipherBlockSize = aes.BlockSize	fileMagic           = "RCLONE\x00\x00"	fileMagicSize       = len(fileMagic)	fileNonceSize       = 24	fileHeaderSize      = fileMagicSize + fileNonceSize	blockHeaderSize     = secretbox.Overhead	blockDataSize       = 64 * 1024	blockSize           = blockHeaderSize + blockDataSize	encryptedSuffix     = ".bin" // when file name encryption is off we add this suffix to make sure the cloud provider doesn't process the file) // Errors returned by ciphervar (	ErrorBadDecryptUTF8          = errors.New("bad decryption - utf-8 invalid")	ErrorBadDecryptControlChar   = errors.New("bad decryption - contains control chars")	ErrorNotAMultipleOfBlocksize = errors.New("not a multiple of blocksize")	ErrorTooShortAfterDecode     = errors.New("too short after base32 decode")	ErrorTooLongAfterDecode      = errors.New("too long after base32 decode")	ErrorEncryptedFileTooShort   = errors.New("file is too short to be encrypted")	ErrorEncryptedFileBadHeader  = errors.New("file has truncated block header")	ErrorEncryptedBadMagic       = errors.New("not an encrypted file - bad magic string")	ErrorEncryptedBadBlock       = errors.New("failed to authenticate decrypted block - bad password?")	ErrorBadBase32Encoding       = errors.New("bad base32 filename encoding")	ErrorFileClosed              = errors.New("file already closed")	ErrorNotAnEncryptedFile      = errors.New("not an encrypted file - no \"" + encryptedSuffix + "\" suffix")	ErrorBadSeek                 = errors.New("Seek beyond end of file")	defaultSalt                  = []byte{0xA8, 0x0D, 0xF4, 0x3A, 0x8F, 0xBD, 0x03, 0x08, 0xA7, 0xCA, 0xB8, 0x3E, 0x58, 0x1F, 0x86, 0xB1}	obfuscQuoteRune              = '!') // Global variablesvar (	fileMagicBytes = []byte(fileMagic)) // ReadSeekCloser is the interface of the read handlestype ReadSeekCloser interface {	io.Reader	io.Seeker	io.Closer	fs.RangeSeeker} // OpenRangeSeek opens the file handle at the offset with the limit giventype OpenRangeSeek func(ctx context.Context, offset, limit int64) (io.ReadCloser, error) // NameEncryptionMode is the type of file name encryption in usetype NameEncryptionMode int // NameEncryptionMode levelsconst (	NameEncryptionOff NameEncryptionMode = iota	NameEncryptionStandard	NameEncryptionObfuscated) // NewNameEncryptionMode turns a string into a NameEncryptionModefunc NewNameEncryptionMode(s string) (mode NameEncryptionMode, err error) {	s = strings.ToLower(s)	switch s {	case "off":		mode = NameEncryptionOff	case "standard":		mode = NameEncryptionStandard	case "obfuscate":		mode = NameEncryptionObfuscated	default:		err = errors.Errorf("Unknown file name encryption mode %q", s)	}	return mode, err} // String turns mode into a human readable stringfunc (mode NameEncryptionMode) String() (out string) {	switch mode {	case NameEncryptionOff:		out = "off"	case NameEncryptionStandard:		out = "standard"	case NameEncryptionObfuscated:		out = "obfuscate"	default:		out = fmt.Sprintf("Unknown mode #%d", mode)	}	return out} // Cipher defines an encoding and decoding cipher for the crypt backendtype Cipher struct {	dataKey        [32]byte                  // Key for secretbox	nameKey        [32]byte                  // 16,24 or 32 bytes	nameTweak      [nameCipherBlockSize]byte // used to tweak the name crypto	block          gocipher.Block	mode           NameEncryptionMode	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 valfunc newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bool) (*Cipher, error) {	c := &Cipher{		mode:           mode,		cryptoRand:     rand.Reader,		dirNameEncrypt: dirNameEncrypt,	}	c.buffers.New = func() interface{} {		return make([]byte, blockSize)	}	err := c.Key(password, salt)	if err != nil {		return nil, err	}	return c, nil} // Key creates all the internal keys from the password passed in using// scrypt.//// If salt is "" we use a fixed salt just to make attackers lives// slighty harder than using no salt.//// Note that empty passsword makes all 0x00 keys which is used in the// tests.func (c *Cipher) Key(password, salt string) (err error) {	const keySize = len(c.dataKey) + len(c.nameKey) + len(c.nameTweak)	var saltBytes = defaultSalt	if salt != "" {		saltBytes = []byte(salt)	}	var key []byte	if password == "" {		key = make([]byte, keySize)	} else {		key, err = scrypt.Key([]byte(password), saltBytes, 16384, 8, 1, keySize)		if err != nil {			return err		}	}	copy(c.dataKey[:], key)	copy(c.nameKey[:], key[len(c.dataKey):])	copy(c.nameTweak[:], key[len(c.dataKey)+len(c.nameKey):])	// Key the name cipher	c.block, err = aes.NewCipher(c.nameKey[:])	return err} // getBlock gets a block from the pool of size blockSizefunc (c *Cipher) getBlock() []byte {	return c.buffers.Get().([]byte)} // putBlock returns a block to the pool of size blockSizefunc (c *Cipher) putBlock(buf []byte) {	if len(buf) != blockSize {		panic("bad blocksize returned to pool")	}	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 encodeFileNamefunc 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//// EME (ECB-Mix-ECB) is a wide-block encryption mode presented in the// 2003 paper "A Parallelizable Enciphering Mode" by Halevi and// Rogaway.//// This makes for deterministic encryption which is what we want - the// same filename must encrypt to the same thing.//// This means that//  * filenames with the same name will encrypt the same//  * filenames which start the same won't have a common prefixfunc (c *Cipher) encryptSegment(plaintext string) string {	if plaintext == "" {		return ""	}	paddedPlaintext := pkcs7.Pad(nameCipherBlockSize, []byte(plaintext))	ciphertext := eme.Transform(c.block, c.nameTweak[:], paddedPlaintext, eme.DirectionEncrypt)	return encodeFileName(ciphertext)} // decryptSegment decrypts a path segmentfunc (c *Cipher) decryptSegment(ciphertext string) (string, error) {	if ciphertext == "" {		return "", nil	}	rawCiphertext, err := decodeFileName(ciphertext)	if err != nil {		return "", err	}	if len(rawCiphertext)%nameCipherBlockSize != 0 {		return "", ErrorNotAMultipleOfBlocksize	}	if len(rawCiphertext) == 0 {		// not possible if decodeFilename() working correctly		return "", ErrorTooShortAfterDecode	}	if len(rawCiphertext) > 2048 {		return "", ErrorTooLongAfterDecode	}	paddedPlaintext := eme.Transform(c.block, c.nameTweak[:], rawCiphertext, eme.DirectionDecrypt)	plaintext, err := pkcs7.Unpad(nameCipherBlockSize, paddedPlaintext)	if err != nil {		return "", err	}	return string(plaintext), err} // Simple obfuscation routinesfunc (c *Cipher) obfuscateSegment(plaintext string) string {	if plaintext == "" {		return ""	} 	// If the string isn't valid UTF8 then don't rotate; just	// prepend a !.	if !utf8.ValidString(plaintext) {		return "!." + plaintext	} 	// Calculate a simple rotation based on the filename and	// the nameKey	var dir int	for _, runeValue := range plaintext {		dir += int(runeValue)	}	dir = dir % 256 	// We'll use this number to store in the result filename...	var result bytes.Buffer	_, _ = result.WriteString(strconv.Itoa(dir) + ".") 	// but we'll augment it with the nameKey for real calculation	for i := 0; i < len(c.nameKey); i++ {		dir += int(c.nameKey[i])	} 	// Now for each character, depending on the range it is in	// we will actually rotate a different amount	for _, runeValue := range plaintext {		switch {		case runeValue == obfuscQuoteRune:			// Quote the Quote character			_, _ = result.WriteRune(obfuscQuoteRune)			_, _ = result.WriteRune(obfuscQuoteRune) 		case runeValue >= '0' && runeValue <= '9':			// Number			thisdir := (dir % 9) + 1			newRune := '0' + (int(runeValue)-'0'+thisdir)%10			_, _ = result.WriteRune(rune(newRune)) 		case (runeValue >= 'A' && runeValue <= 'Z') ||			(runeValue >= 'a' && runeValue <= 'z'):			// ASCII letter.  Try to avoid trivial A->a mappings			thisdir := dir%25 + 1			// Calculate the offset of this character in A-Za-z			pos := int(runeValue - 'A')			if pos >= 26 {				pos -= 6 // It's lower case			}			// Rotate the character to the new location			pos = (pos + thisdir) % 52			if pos >= 26 {				pos += 6 // and handle lower case offset again			}			_, _ = result.WriteRune(rune('A' + pos)) 		case runeValue >= 0xA0 && runeValue <= 0xFF:			// Latin 1 supplement			thisdir := (dir % 95) + 1			newRune := 0xA0 + (int(runeValue)-0xA0+thisdir)%96			_, _ = result.WriteRune(rune(newRune)) 		case runeValue >= 0x100:			// Some random Unicode range; we have no good rules here			thisdir := (dir % 127) + 1			base := int(runeValue - runeValue%256)			newRune := rune(base + (int(runeValue)-base+thisdir)%256)			// If the new character isn't a valid UTF8 char			// then don't rotate it.  Quote it instead			if !utf8.ValidRune(newRune) {				_, _ = result.WriteRune(obfuscQuoteRune)				_, _ = result.WriteRune(runeValue)			} else {				_, _ = result.WriteRune(newRune)			} 		default:			// Leave character untouched			_, _ = result.WriteRune(runeValue)		}	}	return result.String()} func (c *Cipher) deobfuscateSegment(ciphertext string) (string, error) {	if ciphertext == "" {		return "", nil	}	pos := strings.Index(ciphertext, ".")	if pos == -1 {		return "", ErrorNotAnEncryptedFile	} // No .	num := ciphertext[:pos]	if num == "!" {		// No rotation; probably original was not valid unicode		return ciphertext[pos+1:], nil	}	dir, err := strconv.Atoi(num)	if err != nil {		return "", ErrorNotAnEncryptedFile // Not a number	} 	// add the nameKey to get the real rotate distance	for i := 0; i < len(c.nameKey); i++ {		dir += int(c.nameKey[i])	} 	var result bytes.Buffer 	inQuote := false	for _, runeValue := range ciphertext[pos+1:] {		switch {		case inQuote:			_, _ = result.WriteRune(runeValue)			inQuote = false 		case runeValue == obfuscQuoteRune:			inQuote = true 		case runeValue >= '0' && runeValue <= '9':			// Number			thisdir := (dir % 9) + 1			newRune := '0' + int(runeValue) - '0' - thisdir			if newRune < '0' {				newRune += 10			}			_, _ = result.WriteRune(rune(newRune)) 		case (runeValue >= 'A' && runeValue <= 'Z') ||			(runeValue >= 'a' && runeValue <= 'z'):			thisdir := dir%25 + 1			pos := int(runeValue - 'A')			if pos >= 26 {				pos -= 6			}			pos = pos - thisdir			if pos < 0 {				pos += 52			}			if pos >= 26 {				pos += 6			}			_, _ = result.WriteRune(rune('A' + pos)) 		case runeValue >= 0xA0 && runeValue <= 0xFF:			thisdir := (dir % 95) + 1			newRune := 0xA0 + int(runeValue) - 0xA0 - thisdir			if newRune < 0xA0 {				newRune += 96			}			_, _ = result.WriteRune(rune(newRune)) 		case runeValue >= 0x100:			thisdir := (dir % 127) + 1			base := int(runeValue - runeValue%256)			newRune := rune(base + (int(runeValue) - base - thisdir))			if int(newRune) < base {				newRune += 256			}			_, _ = result.WriteRune(newRune) 		default:			_, _ = result.WriteRune(runeValue) 		}	} 	return result.String(), nil} // encryptFileName encrypts a file pathfunc (c *Cipher) encryptFileName(in string) string {	segments := strings.Split(in, "/")	for i := range segments {		// Skip directory name encryption if the user chose to		// leave them intact		if !c.dirNameEncrypt && i != (len(segments)-1) {			continue		}		if c.mode == NameEncryptionStandard {			segments[i] = c.encryptSegment(segments[i])		} else {			segments[i] = c.obfuscateSegment(segments[i])		}	}	return strings.Join(segments, "/")} // EncryptFileName encrypts a file pathfunc (c *Cipher) EncryptFileName(in string) string {	if c.mode == NameEncryptionOff {		return in + encryptedSuffix	}	return c.encryptFileName(in)} // EncryptDirName encrypts a directory pathfunc (c *Cipher) EncryptDirName(in string) string {	if c.mode == NameEncryptionOff || !c.dirNameEncrypt {		return in	}	return c.encryptFileName(in)} // decryptFileName decrypts a file pathfunc (c *Cipher) decryptFileName(in string) (string, error) {	segments := strings.Split(in, "/")	for i := range segments {		var err error		// Skip directory name decryption if the user chose to		// leave them intact		if !c.dirNameEncrypt && i != (len(segments)-1) {			continue		}		if c.mode == NameEncryptionStandard {			segments[i], err = c.decryptSegment(segments[i])		} else {			segments[i], err = c.deobfuscateSegment(segments[i])		} 		if err != nil {			return "", err		}	}	return strings.Join(segments, "/"), nil} // DecryptFileName decrypts a file pathfunc (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		}		return "", ErrorNotAnEncryptedFile

Lines 1–500 of 1054.