package crypt import ( "bytes" "context" "crypto/aes" gocipher "crypto/cipher" "crypto/rand" "encoding/base32" "encoding/base64" "errors" "fmt" "io" "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/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 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 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 - 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 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 } // 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, } 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 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() []byte { return c.buffers.Get().([]byte) } // putBlock returns a block to the pool of size blockSize func (c *Cipher) putBlock(buf []byte) { if len(buf) != blockSize { panic("bad blocksize returned to pool") } 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 = 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 path func (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 } // Strip version string so that only the non-versioned part // of the file name gets encrypted/obfuscated hasVersion := false var t time.Time if i == (len(segments)-1) && version.Match(segments[i]) { var s string t, s = version.Remove(segments[i]) // version.Remove can fail, in which case it returns segments[i] if s != segments[i] { segments[i] = s hasVersion = true } } if c.mode == NameEncryptionStandard { segments[i] = c.encryptSegment(segments[i]) } else { segments[i] = c.obfuscateSegment(segments[i]) } // Add back a version to the encrypted/obfuscated // file name, if we stripped it off earlier if hasVersion { segments[i] = version.Add(segments[i], t) } } return strings.Join(segments, "/") } // EncryptFileName encrypts a file path func (c *Cipher) EncryptFileName(in string) string { if c.mode == NameEncryptionOff { return in + encryptedSuffix } return c.encryptFileName(in) } // EncryptDirName encrypts a directory path func (c *Cipher) EncryptDirName(in string) string { if c.mode == NameEncryptionOff || !c.dirNameEncrypt { return in } return c.encryptFileName(in) } // decryptFileName decrypts a file path func (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 } // Strip version string so that only the non-versioned part // of the file name gets decrypted/deobfuscated hasVersion := false var t time.Time if i == (len(segments)-1) && version.Match(segments[i]) { var s string t, s = version.Remove(segments[i]) // version.Remove can fail, in which case it returns segments[i] if s != segments[i] { segments[i] = s hasVersion = true } } if c.mode == NameEncryptionStandard { segments[i], err = c.decryptSegment(segments[i]) } else { segments[i], err = c.deobfuscateSegment(segments[i]) } if err != nil { return "", err } // Add back a version to the decrypted/deobfuscated // file name, if we stripped it off earlier if hasVersion { segments[i] = version.Add(segments[i], t) } } return strings.Join(segments, "/"), nil } // DecryptFileName decrypts a file path func (c *Cipher) DecryptFileName(in string) (string, error) { if c.mode == NameEncryptionOff { remainingLength := len(in) - len(encryptedSuffix) if remainingLength == 0 || !strings.HasSuffix(in, encryptedSuffix) { return "", ErrorNotAnEncryptedFile } decrypted := in[:remainingLength] if version.Match(decrypted) { _, unversioned := version.Remove(decrypted) if unversioned == "" { return "", ErrorNotAnEncryptedFile } } // Leave the version string on, if it was there return decrypted, nil } return c.decryptFileName(in) } // DecryptDirName decrypts a directory path func (c *Cipher) DecryptDirName(in string) (string, error) { if c.mode == NameEncryptionOff || !c.dirNameEncrypt { return in, nil } return c.decryptFileName(in) } // NameEncryptionMode returns the encryption mode in use for names func (c *Cipher) NameEncryptionMode() NameEncryptionMode { return c.mode } // nonce is an NACL secretbox nonce type nonce [fileNonceSize]byte // pointer returns the nonce as a *[24]byte for secretbox func (n *nonce) pointer() *[fileNonceSize]byte { return (*[fileNonceSize]byte)(n) } // fromReader fills the nonce from an io.Reader - normally the OSes // crypto random number generator func (n *nonce) fromReader(in io.Reader) error { read, err := io.ReadFull(in, (*n)[:]) if read != fileNonceSize { return fmt.Errorf("short read of nonce: %w", err) } return nil } // fromBuf fills the nonce from the buffer passed in func (n *nonce) fromBuf(buf []byte) { read := copy((*n)[:], buf) if read != fileNonceSize { panic("buffer to short to read nonce") } } // carry 1 up the nonce from position i func (n *nonce) carry(i int) { for ; i < len(*n); i++ { digit := (*n)[i] newDigit := digit + 1 (*n)[i] = newDigit if newDigit >= digit { // exit if no carry break } } } // increment to add 1 to the nonce func (n *nonce) increment() { n.carry(0) } // add a uint64 to the nonce func (n *nonce) add(x uint64) { carry := uint16(0) for i := 0; i < 8; i++ { digit := (*n)[i] xDigit := byte(x) x >>= 8 carry += uint16(digit) + uint16(xDigit) (*n)[i] = byte(carry) carry >>= 8 } if carry != 0 { n.carry(8) } } // encrypter encrypts an io.Reader on the fly type encrypter struct { mu sync.Mutex in io.Reader c *Cipher nonce nonce buf []byte readBuf []byte bufIndex int bufSize int err error } // newEncrypter creates a new file handle encrypting on the fly func (c *Cipher) newEncrypter(in io.Reader, nonce *nonce) (*encrypter, error) { fh := &encrypter{ in: in, c: c, buf: c.getBlock(), readBuf: c.getBlock(), bufSize: fileHeaderSize, } // Initialise nonce if nonce != nil { fh.nonce = *nonce } else { err := fh.nonce.fromReader(c.cryptoRand) if err != nil { return nil, err } } // Copy magic into buffer copy(fh.buf, fileMagicBytes) // Copy nonce into buffer copy(fh.buf[fileMagicSize:], fh.nonce[:]) return fh, nil } // Read as per io.Reader func (fh *encrypter) Read(p []byte) (n int, err error) { fh.mu.Lock() defer fh.mu.Unlock() if fh.err != nil { return 0, fh.err } if fh.bufIndex >= fh.bufSize { // Read data // FIXME should overlap the reads with a go-routine and 2 buffers? readBuf := fh.readBuf[:blockDataSize] n, err = io.ReadFull(fh.in, readBuf) if n == 0 { // err can't be nil since: // n == len(buf) if and only if err == nil. return fh.finish(err) } // possibly err != nil here, but we will process the // data and the next call to ReadFull will return 0, err // Encrypt the block using the nonce secretbox.Seal(fh.buf[:0], readBuf[:n], fh.nonce.pointer(), &fh.c.dataKey) fh.bufIndex = 0 fh.bufSize = blockHeaderSize + n fh.nonce.increment() } n = copy(p, fh.buf[fh.bufIndex:fh.bufSize]) fh.bufIndex += n return n, nil } // finish sets the final error and tidies up func (fh *encrypter) finish(err error) (int, error) { if fh.err != nil { return 0, fh.err } fh.err = err fh.c.putBlock(fh.buf) fh.buf = nil fh.c.putBlock(fh.readBuf) fh.readBuf = nil return 0, err } // Encrypt data encrypts the data stream func (c *Cipher) encryptData(in io.Reader) (io.Reader, *encrypter, error) { in, wrap := accounting.UnWrap(in) // unwrap the accounting off the Reader out, err := c.newEncrypter(in, nil) if err != nil { return nil, nil, err } return wrap(out), out, nil // and wrap the accounting back on } // EncryptData encrypts the data stream func (c *Cipher) EncryptData(in io.Reader) (io.Reader, error) { out, _, err := c.encryptData(in) return out, err } // decrypter decrypts an io.ReaderCloser on the fly type decrypter struct { mu sync.Mutex rc io.ReadCloser nonce nonce initialNonce nonce c *Cipher buf []byte readBuf []byte bufIndex int bufSize int err error limit int64 // limit of bytes to read, -1 for unlimited open OpenRangeSeek } // newDecrypter creates a new file handle decrypting on the fly func (c *Cipher) newDecrypter(rc io.ReadCloser) (*decrypter, error) { fh := &decrypter{ rc: rc, c: c, buf: c.getBlock(), readBuf: c.getBlock(), limit: -1, } // Read file header (magic + nonce) readBuf := fh.readBuf[:fileHeaderSize] _, err := io.ReadFull(fh.rc, readBuf) if err == io.EOF || err == io.ErrUnexpectedEOF { // This read from 0..fileHeaderSize-1 bytes return nil, fh.finishAndClose(ErrorEncryptedFileTooShort) } else if err != nil { return nil, fh.finishAndClose(err) } // check the magic if !bytes.Equal(readBuf[:fileMagicSize], fileMagicBytes) { return nil, fh.finishAndClose(ErrorEncryptedBadMagic) } // retrieve the nonce fh.nonce.fromBuf(readBuf[fileMagicSize:]) fh.initialNonce = fh.nonce return fh, nil } // newDecrypterSeek creates a new file handle decrypting on the fly func (c *Cipher) newDecrypterSeek(ctx context.Context, open OpenRangeSeek, offset, limit int64) (fh *decrypter, err error) { var rc io.ReadCloser doRangeSeek := false setLimit := false // Open initially with no seek if offset == 0 && limit < 0 { // If no offset or limit then open whole file rc, err = open(ctx, 0, -1) } else if offset == 0 { // If no offset open the header + limit worth of the file _, underlyingLimit, _, _ := calculateUnderlying(offset, limit) rc, err = open(ctx, 0, int64(fileHeaderSize)+underlyingLimit) setLimit = true } else { // Otherwise just read the header to start with rc, err = open(ctx, 0, int64(fileHeaderSize)) doRangeSeek = true } if err != nil { return nil, err } // Open the stream which fills in the nonce fh, err = c.newDecrypter(rc) if err != nil { return nil, err } fh.open = open // will be called by fh.RangeSeek if doRangeSeek { _, err = fh.RangeSeek(ctx, offset, io.SeekStart, limit) if err != nil { _ = fh.Close() return nil, err } } if setLimit { fh.limit = limit } return fh, nil } // read data into internal buffer - call with fh.mu held func (fh *decrypter) fillBuffer() (err error) { // FIXME should overlap the reads with a go-routine and 2 buffers? readBuf := fh.readBuf n, err := io.ReadFull(fh.rc, readBuf) if n == 0 { // err can't be nil since: // n == len(buf) if and only if err == nil. return err } // possibly err != nil here, but we will process the data and // the next call to ReadFull will return 0, err // Check header + 1 byte exists if n <= blockHeaderSize { if err != nil { return err // return pending error as it is likely more accurate } return ErrorEncryptedFileBadHeader } // Decrypt the block using the nonce _, ok := secretbox.Open(fh.buf[:0], readBuf[:n], fh.nonce.pointer(), &fh.c.dataKey) if !ok { if err != nil { return err // return pending error as it is likely more accurate } return ErrorEncryptedBadBlock } fh.bufIndex = 0 fh.bufSize = n - blockHeaderSize fh.nonce.increment() return nil } // Read as per io.Reader func (fh *decrypter) Read(p []byte) (n int, err error) { fh.mu.Lock() defer fh.mu.Unlock() if fh.err != nil { return 0, fh.err } if fh.bufIndex >= fh.bufSize { err = fh.fillBuffer() if err != nil { return 0, fh.finish(err) } } toCopy := fh.bufSize - fh.bufIndex if fh.limit >= 0 && fh.limit < int64(toCopy) { toCopy = int(fh.limit) } n = copy(p, fh.buf[fh.bufIndex:fh.bufIndex+toCopy]) fh.bufIndex += n if fh.limit >= 0 { fh.limit -= int64(n) if fh.limit == 0 { return n, fh.finish(io.EOF) } } return n, nil } // calculateUnderlying converts an (offset, limit) in a crypted file // into an (underlyingOffset, underlyingLimit) for the underlying // file. // // It also returns number of bytes to discard after reading the first // block and number of blocks this is from the start so the nonce can // be incremented. func calculateUnderlying(offset, limit int64) (underlyingOffset, underlyingLimit, discard, blocks int64) { // blocks we need to seek, plus bytes we need to discard blocks, discard = offset/blockDataSize, offset%blockDataSize // Offset in underlying stream we need to seek underlyingOffset = int64(fileHeaderSize) + blocks*(blockHeaderSize+blockDataSize) // work out how many blocks we need to read underlyingLimit = int64(-1) if limit >= 0 { // bytes to read beyond the first block bytesToRead := limit - (blockDataSize - discard) // Read the first block blocksToRead := int64(1) if bytesToRead > 0 { // Blocks that need to be read plus left over blocks extraBlocksToRead, endBytes := bytesToRead/blockDataSize, bytesToRead%blockDataSize if endBytes != 0 { // If left over bytes must read another block extraBlocksToRead++ } blocksToRead += extraBlocksToRead } // Must read a whole number of blocks underlyingLimit = blocksToRead * (blockHeaderSize + blockDataSize) } return } // RangeSeek behaves like a call to Seek(offset int64, whence // int) with the output wrapped in an io.LimitedReader // limiting the total length to limit. // // RangeSeek with a limit of < 0 is equivalent to a regular Seek. func (fh *decrypter) RangeSeek(ctx context.Context, offset int64, whence int, limit int64) (int64, error) { fh.mu.Lock() defer fh.mu.Unlock() if fh.open == nil { return 0, fh.finish(errors.New("can't seek - not initialised with newDecrypterSeek")) } if whence != io.SeekStart { return 0, fh.finish(errors.New("can only seek from the start")) } // Reset error or return it if not EOF if fh.err == io.EOF { fh.unFinish() } else if fh.err != nil { return 0, fh.err } underlyingOffset, underlyingLimit, discard, blocks := calculateUnderlying(offset, limit) // Move the nonce on the correct number of blocks from the start fh.nonce = fh.initialNonce fh.nonce.add(uint64(blocks)) // Can we seek underlying stream directly? if do, ok := fh.rc.(fs.RangeSeeker); ok { // Seek underlying stream directly _, err := do.RangeSeek(ctx, underlyingOffset, 0, underlyingLimit) if err != nil { return 0, fh.finish(err) } } else { // if not reopen with seek _ = fh.rc.Close() // close underlying file fh.rc = nil // Re-open the underlying object with the offset given rc, err := fh.open(ctx, underlyingOffset, underlyingLimit) if err != nil { return 0, fh.finish(fmt.Errorf("couldn't reopen file with offset and limit: %w", err)) } // Set the file handle fh.rc = rc } // Fill the buffer err := fh.fillBuffer() if err != nil { return 0, fh.finish(err) } // Discard bytes from the buffer if int(discard) > fh.bufSize { return 0, fh.finish(ErrorBadSeek) } fh.bufIndex = int(discard) // Set the limit fh.limit = limit return offset, nil } // Seek implements the io.Seeker interface func (fh *decrypter) Seek(offset int64, whence int) (int64, error) { return fh.RangeSeek(context.TODO(), offset, whence, -1) } // finish sets the final error and tidies up func (fh *decrypter) finish(err error) error { if fh.err != nil { return fh.err } fh.err = err fh.c.putBlock(fh.buf) fh.buf = nil fh.c.putBlock(fh.readBuf) fh.readBuf = nil return err } // unFinish undoes the effects of finish func (fh *decrypter) unFinish() { // Clear error fh.err = nil // reinstate the buffers fh.buf = fh.c.getBlock() fh.readBuf = fh.c.getBlock() // Empty the buffer fh.bufIndex = 0 fh.bufSize = 0 } // Close func (fh *decrypter) Close() error { fh.mu.Lock() defer fh.mu.Unlock() // Check already closed if fh.err == ErrorFileClosed { return fh.err } // Closed before reading EOF so not finish()ed yet if fh.err == nil { _ = fh.finish(io.EOF) } // Show file now closed fh.err = ErrorFileClosed if fh.rc == nil { return nil } return fh.rc.Close() } // finishAndClose does finish then Close() // // Used when we are returning a nil fh from new func (fh *decrypter) finishAndClose(err error) error { _ = fh.finish(err) _ = fh.Close() return err } // DecryptData decrypts the data stream func (c *Cipher) DecryptData(rc io.ReadCloser) (io.ReadCloser, error) { out, err := c.newDecrypter(rc) if err != nil { return nil, err } return out, nil } // DecryptDataSeek decrypts the data stream from offset // // The open function must return a ReadCloser opened to the offset supplied. // // You must use this form of DecryptData if you might want to Seek the file handle func (c *Cipher) DecryptDataSeek(ctx context.Context, open OpenRangeSeek, offset, limit int64) (ReadSeekCloser, error) { out, err := c.newDecrypterSeek(ctx, open, offset, limit) if err != nil { return nil, err } return out, nil } // EncryptedSize calculates the size of the data when encrypted func (c *Cipher) EncryptedSize(size int64) int64 { blocks, residue := size/blockDataSize, size%blockDataSize encryptedSize := int64(fileHeaderSize) + blocks*(blockHeaderSize+blockDataSize) if residue != 0 { encryptedSize += blockHeaderSize + residue } return encryptedSize } // DecryptedSize calculates the size of the data when decrypted func (c *Cipher) DecryptedSize(size int64) (int64, error) { size -= int64(fileHeaderSize) if size < 0 { return 0, ErrorEncryptedFileTooShort } blocks, residue := size/blockSize, size%blockSize decryptedSize := blocks * blockDataSize if residue != 0 { residue -= blockHeaderSize if residue <= 0 { return 0, ErrorEncryptedFileBadHeader } } decryptedSize += residue return decryptedSize, nil } // check interfaces var ( _ io.ReadCloser = (*decrypter)(nil) _ io.Seeker = (*decrypter)(nil) _ fs.RangeSeeker = (*decrypter)(nil) _ io.Reader = (*encrypter)(nil) )