1181 lines · 31.9 KB · Go
RawDownload
package crypt import (	"bytes"	"context"	"crypto/aes"	gocipher "crypto/cipher"	"crypto/rand"	"encoding/base32"	"encoding/base64"	"errors"	"fmt"	"io"	"path"	"strconv"	"strings"	"sync"	"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"	"github.com/rclone/rclone/lib/readers"	"github.com/rclone/rclone/lib/version"	"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) // 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 - does not match suffix")	ErrorBadSeek                 = errors.New("Seek beyond end of file")	ErrorSuffixMissingDot        = errors.New("suffix config setting should include a '.'")	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 = fmt.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} // fileNameEncoding are the encoding methods dealing with encrypted file namestype 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 string 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 EncodeToStringfunc (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 stringfunc 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 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	fileNameEnc     fileNameEncoding	buffers         sync.Pool // encrypt/decrypt buffers	cryptoRand      io.Reader // read crypto random numbers from here	dirNameEncrypt  bool	passBadBlocks   bool // if set passed bad blocks as zeroed blocks	encryptedSuffix string} // newCipher initialises the cipher.  If salt is "" then it uses a built in salt valfunc newCipher(mode NameEncryptionMode, password, salt string, dirNameEncrypt bool, enc fileNameEncoding) (*Cipher, error) {	c := &Cipher{		mode:            mode,		fileNameEnc:     enc,		cryptoRand:      rand.Reader,		dirNameEncrypt:  dirNameEncrypt,		encryptedSuffix: ".bin",	}	c.buffers.New = func() any {		return new([blockSize]byte)	}	err := c.Key(password, salt)	if err != nil {		return nil, err	}	return c, nil} // setEncryptedSuffix set suffix, or an empty stringfunc (c *Cipher) setEncryptedSuffix(suffix string) {	if strings.EqualFold(suffix, "none") {		c.encryptedSuffix = ""		return	}	if !strings.HasPrefix(suffix, ".") {		fs.Errorf(nil, "crypt: bad suffix: %v", ErrorSuffixMissingDot)		suffix = "." + suffix	}	c.encryptedSuffix = suffix} // Call to set bad block pass throughfunc (c *Cipher) setPassBadBlocks(passBadBlocks bool) {	c.passBadBlocks = passBadBlocks} // 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// slightly harder than using no salt.//// Note that empty password 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() *[blockSize]byte {	return c.buffers.Get().(*[blockSize]byte)} // putBlock returns a block to the pool of size blockSizefunc (c *Cipher) putBlock(buf *[blockSize]byte) {	c.buffers.Put(buf)} // 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 c.fileNameEnc.EncodeToString(ciphertext)} // decryptSegment decrypts a path segmentfunc (c *Cipher) decryptSegment(ciphertext string) (string, error) {	if ciphertext == "" {		return "", nil	}	rawCiphertext, err := c.fileNameEnc.DecodeString(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 %= 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 := range len(c.nameKey) {		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	}	before, after, ok := strings.Cut(ciphertext, ".")	if !ok {		return "", ErrorNotAnEncryptedFile	} // No .	num := before	if num == "!" {		// No rotation; probably original was not valid unicode		return after, 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 := range len(c.nameKey) {		dir += int(c.nameKey[i])	} 	var result bytes.Buffer 	inQuote := false	for _, runeValue := range after {		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 -= 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 path//// 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 {		// Skip directory name encryption if the user chose to		// leave them intact		if !c.dirNameEncrypt && i != (len(segments)-1) {

Lines 1–500 of 1181.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t