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. Load more
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" ) // Constants const ( 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 cipher var ( 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 variables var ( fileMagicBytes = []byte(fileMagic) ) // ReadSeekCloser is the interface of the read handles type ReadSeekCloser interface { io.Reader io.Seeker io.Closer fs.RangeSeeker } // OpenRangeSeek opens the file handle at the offset with the limit given type OpenRangeSeek func(ctx context.Context, offset, limit int64) (io.ReadCloser, error) // NameEncryptionMode is the type of file name encryption in use type NameEncryptionMode int // NameEncryptionMode levels const ( NameEncryptionOff NameEncryptionMode = iota NameEncryptionStandard NameEncryptionObfuscated ) // NewNameEncryptionMode turns a string into a NameEncryptionMode func 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 string func (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 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 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 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 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 val func 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 string func (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 through func (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 blockSize func (c *Cipher) getBlock() *[blockSize]byte { return c.buffers.Get().(*[blockSize]byte) } // putBlock returns a block to the pool of size blockSize func (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 prefix func (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 segment func (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 routines func (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) {