package crypt import ( "bytes" "context" "crypto/aes" gocipher "crypto/cipher" "crypto/rand" "errors" "fmt" "io" "sync" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/scrypt" ) const ( nameCipherBlockSize = aes.BlockSize fileMagic = "TELDRIVE\x00\x00" fileMagicSize = len(fileMagic) fileNonceSize = 24 fileHeaderSize = fileMagicSize + fileNonceSize blockHeaderSize = secretbox.Overhead blockDataSize = 64 * 1024 blockSize = blockHeaderSize + blockDataSize ) var ( 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") ErrorFileClosed = errors.New("file already closed") ErrorBadSeek = errors.New("Seek beyond end of file") ) var ( fileMagicBytes = []byte(fileMagic) ) type ReadSeekCloser interface { io.Reader io.Seeker io.Closer } type OpenRangeSeek func(ctx context.Context, offset, limit int64) (io.ReadCloser, error) func readFill(r io.Reader, buf []byte) (n int, err error) { var nn int for n < len(buf) && err == nil { nn, err = r.Read(buf[n:]) n += nn } return n, err } type Cipher struct { dataKey [32]byte nameKey [32]byte nameTweak [nameCipherBlockSize]byte block gocipher.Block buffers sync.Pool cryptoRand io.Reader } func NewCipher(password, salt string) (*Cipher, error) { c := &Cipher{ cryptoRand: rand.Reader, } c.buffers.New = func() interface{} { return new([blockSize]byte) } err := c.Key(password, salt) if err != nil { return nil, err } return c, nil } func (c *Cipher) Key(password, salt string) (err error) { const keySize = len(c.dataKey) + len(c.nameKey) + len(c.nameTweak) saltBytes := []byte(salt) 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):]) c.block, err = aes.NewCipher(c.nameKey[:]) return err } func (c *Cipher) getBlock() *[blockSize]byte { return c.buffers.Get().(*[blockSize]byte) } func (c *Cipher) putBlock(buf *[blockSize]byte) { c.buffers.Put(buf) } type nonce [fileNonceSize]byte func (n *nonce) pointer() *[fileNonceSize]byte { return (*[fileNonceSize]byte)(n) } func (n *nonce) fromReader(in io.Reader) error { read, err := readFill(in, (*n)[:]) if read != fileNonceSize { return fmt.Errorf("short read of nonce: %w", err) } return nil } func (n *nonce) fromBuf(buf []byte) error { read := copy((*n)[:], buf) if read != fileNonceSize { return errors.New("buffer to short to read nonce") } return nil } 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 } } } func (n *nonce) increment() { n.carry(0) } 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) } } type encrypter struct { mu sync.Mutex in io.Reader c *Cipher nonce nonce buf *[blockSize]byte readBuf *[blockSize]byte bufIndex int bufSize int err error } 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, } if nonce != nil { fh.nonce = *nonce } else { err := fh.nonce.fromReader(c.cryptoRand) if err != nil { return nil, err } } copy((*fh.buf)[:], fileMagicBytes) copy((*fh.buf)[fileMagicSize:], fh.nonce[:]) return fh, nil } 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 { readBuf := (*fh.readBuf)[:blockDataSize] n, err = readFill(fh.in, readBuf) if n == 0 { return fh.finish(err) } 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 } 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 } func (fh *encrypter) Close() error { return nil } func (c *Cipher) EncryptData(in io.Reader) (io.ReadCloser, error) { return c.newEncrypter(in, nil) } type decrypter struct { mu sync.Mutex rc io.ReadCloser nonce nonce initialNonce nonce c *Cipher buf *[blockSize]byte readBuf *[blockSize]byte bufIndex int bufSize int err error limit int64 open OpenRangeSeek } func (c *Cipher) newDecrypter(rc io.ReadCloser) (*decrypter, error) { fh := &decrypter{ rc: rc, c: c, buf: c.getBlock(), readBuf: c.getBlock(), limit: -1, } readBuf := (*fh.readBuf)[:fileHeaderSize] n, err := readFill(fh.rc, readBuf) if n < fileHeaderSize && err == io.EOF { return nil, fh.finishAndClose(ErrorEncryptedFileTooShort) } else if err != io.EOF && err != nil { return nil, fh.finishAndClose(err) } if !bytes.Equal(readBuf[:fileMagicSize], fileMagicBytes) { return nil, fh.finishAndClose(ErrorEncryptedBadMagic) } err = fh.nonce.fromBuf(readBuf[fileMagicSize:]) if err != nil { return nil, err } fh.initialNonce = fh.nonce return fh, nil } func (c *Cipher) newDecrypterSeek(ctx context.Context, open OpenRangeSeek, offset, limit int64) (fh *decrypter, err error) { var rc io.ReadCloser doRangeSeek := false setLimit := false if offset == 0 && limit < 0 { rc, err = open(ctx, 0, -1) } else if offset == 0 { _, underlyingLimit, _, _ := calculateUnderlying(offset, limit) rc, err = open(ctx, 0, int64(fileHeaderSize)+underlyingLimit) setLimit = true } else { rc, err = open(ctx, 0, int64(fileHeaderSize)) doRangeSeek = true } if err != nil { return nil, err } fh, err = c.newDecrypter(rc) if err != nil { return nil, err } fh.open = open 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 } func (fh *decrypter) fillBuffer() (err error) { readBuf := fh.readBuf n, err := readFill(fh.rc, (*readBuf)[:]) if n == 0 { return err } if n <= blockHeaderSize { if err != nil && err != io.EOF { return err } return ErrorEncryptedFileBadHeader } _, ok := secretbox.Open((*fh.buf)[:0], (*readBuf)[:n], fh.nonce.pointer(), &fh.c.dataKey) if !ok { if err != nil && err != io.EOF { return err } for i := range (*fh.buf)[:n] { (*fh.buf)[i] = 0 } } fh.bufIndex = 0 fh.bufSize = n - blockHeaderSize fh.nonce.increment() return nil } 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 } func calculateUnderlying(offset, limit int64) (underlyingOffset, underlyingLimit, discard, blocks int64) { blocks, discard = offset/blockDataSize, offset%blockDataSize underlyingOffset = int64(fileHeaderSize) + blocks*(blockHeaderSize+blockDataSize) underlyingLimit = int64(-1) if limit >= 0 { bytesToRead := limit - (blockDataSize - discard) blocksToRead := int64(1) if bytesToRead > 0 { extraBlocksToRead, endBytes := bytesToRead/blockDataSize, bytesToRead%blockDataSize if endBytes != 0 { extraBlocksToRead++ } blocksToRead += extraBlocksToRead } underlyingLimit = blocksToRead * (blockHeaderSize + blockDataSize) } return } 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")) } if fh.err == io.EOF { fh.unFinish() } else if fh.err != nil { return 0, fh.err } underlyingOffset, underlyingLimit, discard, blocks := calculateUnderlying(offset, limit) fh.nonce = fh.initialNonce fh.nonce.add(uint64(blocks)) 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)) } fh.rc = rc err = fh.fillBuffer() if err != nil { return 0, fh.finish(err) } if int(discard) > fh.bufSize { return 0, fh.finish(ErrorBadSeek) } fh.bufIndex = int(discard) fh.limit = limit return offset, nil } func (fh *decrypter) Seek(offset int64, whence int) (int64, error) { return fh.RangeSeek(context.TODO(), offset, whence, -1) } 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 } func (fh *decrypter) unFinish() { fh.err = nil fh.buf = fh.c.getBlock() fh.readBuf = fh.c.getBlock() fh.bufIndex = 0 fh.bufSize = 0 } func (fh *decrypter) Close() error { fh.mu.Lock() defer fh.mu.Unlock() if fh.err == ErrorFileClosed { return fh.err } if fh.err == nil { _ = fh.finish(io.EOF) } fh.err = ErrorFileClosed if fh.rc == nil { return nil } return fh.rc.Close() } func (fh *decrypter) finishAndClose(err error) error { _ = fh.finish(err) _ = fh.Close() return err } func (c *Cipher) DecryptData(rc io.ReadCloser) (io.ReadCloser, error) { out, err := c.newDecrypter(rc) if err != nil { return nil, err } return out, nil } 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 } func EncryptedSize(size int64) int64 { blocks, residue := size/blockDataSize, size%blockDataSize encryptedSize := int64(fileHeaderSize) + blocks*(blockHeaderSize+blockDataSize) if residue != 0 { encryptedSize += blockHeaderSize + residue } return encryptedSize } func 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 }