123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- // Copyright (C) 2019-2023 Nicola Murino
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published
- // by the Free Software Foundation, version 3.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package vfs
- import (
- "bytes"
- "crypto/rand"
- "crypto/sha256"
- "fmt"
- "io"
- "net/http"
- "os"
- "github.com/eikenb/pipeat"
- "github.com/minio/sio"
- "golang.org/x/crypto/hkdf"
- "github.com/drakkan/sftpgo/v2/internal/logger"
- )
- const (
- // cryptFsName is the name for the local Fs implementation with encryption support
- cryptFsName = "cryptfs"
- version10 byte = 0x10
- nonceV10Size int = 32
- headerV10Size int64 = 33 // 1 (version byte) + 32 (nonce size)
- )
- // CryptFs is a Fs implementation that allows to encrypts/decrypts local files
- type CryptFs struct {
- *OsFs
- localTempDir string
- masterKey []byte
- }
- // NewCryptFs returns a CryptFs object
- func NewCryptFs(connectionID, rootDir, mountPath string, config CryptFsConfig) (Fs, error) {
- if err := config.validate(); err != nil {
- return nil, err
- }
- if err := config.Passphrase.TryDecrypt(); err != nil {
- return nil, err
- }
- fs := &CryptFs{
- OsFs: &OsFs{
- name: cryptFsName,
- connectionID: connectionID,
- rootDir: rootDir,
- mountPath: getMountPath(mountPath),
- },
- masterKey: []byte(config.Passphrase.GetPayload()),
- }
- if tempPath == "" {
- fs.localTempDir = rootDir
- } else {
- fs.localTempDir = tempPath
- }
- return fs, nil
- }
- // Name returns the name for the Fs implementation
- func (fs *CryptFs) Name() string {
- return fs.name
- }
- // Open opens the named file for reading
- func (fs *CryptFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
- f, key, err := fs.getFileAndEncryptionKey(name)
- if err != nil {
- return nil, nil, nil, err
- }
- isZeroDownload, err := isZeroBytesDownload(f, offset)
- if err != nil {
- f.Close()
- return nil, nil, nil, err
- }
- r, w, err := pipeat.PipeInDir(fs.localTempDir)
- if err != nil {
- f.Close()
- return nil, nil, nil, err
- }
- go func() {
- if isZeroDownload {
- w.CloseWithError(err) //nolint:errcheck
- f.Close()
- fsLog(fs, logger.LevelDebug, "zero bytes download completed, path: %q", name)
- return
- }
- var n int64
- var err error
- if offset == 0 {
- n, err = sio.Decrypt(w, f, fs.getSIOConfig(key))
- } else {
- var readerAt io.ReaderAt
- var readed, written int
- buf := make([]byte, 65536)
- wrapper := &cryptedFileWrapper{
- File: f,
- }
- readerAt, err = sio.DecryptReaderAt(wrapper, fs.getSIOConfig(key))
- if err == nil {
- finished := false
- for !finished {
- readed, err = readerAt.ReadAt(buf, offset)
- offset += int64(readed)
- if err != nil && err != io.EOF {
- break
- }
- if err == io.EOF {
- finished = true
- err = nil
- }
- if readed > 0 {
- written, err = w.Write(buf[:readed])
- n += int64(written)
- if err != nil {
- if err == io.EOF {
- err = io.ErrUnexpectedEOF
- }
- break
- }
- if readed != written {
- err = io.ErrShortWrite
- break
- }
- }
- }
- }
- }
- w.CloseWithError(err) //nolint:errcheck
- f.Close()
- fsLog(fs, logger.LevelDebug, "download completed, path: %q size: %v, err: %v", name, n, err)
- }()
- return nil, r, nil, nil
- }
- // Create creates or opens the named file for writing
- func (fs *CryptFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
- var err error
- var f *os.File
- if flag == 0 {
- f, err = os.Create(name)
- } else {
- f, err = os.OpenFile(name, flag, 0666)
- }
- if err != nil {
- return nil, nil, nil, err
- }
- header := encryptedFileHeader{
- version: version10,
- nonce: make([]byte, 32),
- }
- _, err = io.ReadFull(rand.Reader, header.nonce)
- if err != nil {
- f.Close()
- return nil, nil, nil, err
- }
- var key [32]byte
- kdf := hkdf.New(sha256.New, fs.masterKey, header.nonce, nil)
- _, err = io.ReadFull(kdf, key[:])
- if err != nil {
- f.Close()
- return nil, nil, nil, err
- }
- r, w, err := pipeat.PipeInDir(fs.localTempDir)
- if err != nil {
- f.Close()
- return nil, nil, nil, err
- }
- err = header.Store(f)
- if err != nil {
- r.Close()
- w.Close()
- f.Close()
- return nil, nil, nil, err
- }
- p := NewPipeWriter(w)
- go func() {
- n, err := sio.Encrypt(f, r, fs.getSIOConfig(key))
- errClose := f.Close()
- if err == nil && errClose != nil {
- err = errClose
- }
- r.CloseWithError(err) //nolint:errcheck
- p.Done(err)
- fsLog(fs, logger.LevelDebug, "upload completed, path: %q, readed bytes: %v, err: %v", name, n, err)
- }()
- return nil, p, nil, nil
- }
- // Truncate changes the size of the named file
- func (*CryptFs) Truncate(_ string, _ int64) error {
- return ErrVfsUnsupported
- }
- // ReadDir reads the directory named by dirname and returns
- // a list of directory entries.
- func (fs *CryptFs) ReadDir(dirname string) ([]os.FileInfo, error) {
- f, err := os.Open(dirname)
- if err != nil {
- return nil, err
- }
- list, err := f.Readdir(-1)
- f.Close()
- if err != nil {
- return nil, err
- }
- result := make([]os.FileInfo, 0, len(list))
- for _, info := range list {
- result = append(result, fs.ConvertFileInfo(info))
- }
- return result, nil
- }
- // IsUploadResumeSupported returns false sio does not support random access writes
- func (*CryptFs) IsUploadResumeSupported() bool {
- return false
- }
- // GetMimeType returns the content type
- func (fs *CryptFs) GetMimeType(name string) (string, error) {
- f, key, err := fs.getFileAndEncryptionKey(name)
- if err != nil {
- return "", err
- }
- defer f.Close()
- readSize, err := sio.DecryptedSize(512)
- if err != nil {
- return "", err
- }
- buf := make([]byte, readSize)
- n, err := io.ReadFull(f, buf)
- if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
- return "", err
- }
- decrypted := bytes.NewBuffer(nil)
- _, err = sio.Decrypt(decrypted, bytes.NewBuffer(buf[:n]), fs.getSIOConfig(key))
- if err != nil {
- return "", err
- }
- ctype := http.DetectContentType(decrypted.Bytes())
- // Rewind file.
- _, err = f.Seek(0, io.SeekStart)
- return ctype, err
- }
- func (fs *CryptFs) getSIOConfig(key [32]byte) sio.Config {
- return sio.Config{
- MinVersion: sio.Version20,
- MaxVersion: sio.Version20,
- Key: key[:],
- }
- }
- // ConvertFileInfo returns a FileInfo with the decrypted size
- func (fs *CryptFs) ConvertFileInfo(info os.FileInfo) os.FileInfo {
- if !info.Mode().IsRegular() {
- return info
- }
- size := info.Size()
- if size >= headerV10Size {
- size -= headerV10Size
- decryptedSize, err := sio.DecryptedSize(uint64(size))
- if err == nil {
- size = int64(decryptedSize)
- }
- } else {
- size = 0
- }
- return NewFileInfo(info.Name(), info.IsDir(), size, info.ModTime(), false)
- }
- func (fs *CryptFs) getFileAndEncryptionKey(name string) (*os.File, [32]byte, error) {
- var key [32]byte
- f, err := os.Open(name)
- if err != nil {
- return nil, key, err
- }
- header := encryptedFileHeader{}
- err = header.Load(f)
- if err != nil {
- f.Close()
- return nil, key, err
- }
- kdf := hkdf.New(sha256.New, fs.masterKey, header.nonce, nil)
- _, err = io.ReadFull(kdf, key[:])
- if err != nil {
- f.Close()
- return nil, key, err
- }
- return f, key, err
- }
- func isZeroBytesDownload(f *os.File, offset int64) (bool, error) {
- info, err := f.Stat()
- if err != nil {
- return false, err
- }
- if info.Size() == headerV10Size {
- return true, nil
- }
- if info.Size() > headerV10Size {
- decSize, err := sio.DecryptedSize(uint64(info.Size() - headerV10Size))
- if err != nil {
- return false, err
- }
- if int64(decSize) == offset {
- return true, nil
- }
- }
- return false, nil
- }
- type encryptedFileHeader struct {
- version byte
- nonce []byte
- }
- func (h *encryptedFileHeader) Store(f *os.File) error {
- buf := make([]byte, 0, headerV10Size)
- buf = append(buf, version10)
- buf = append(buf, h.nonce...)
- _, err := f.Write(buf)
- return err
- }
- func (h *encryptedFileHeader) Load(f *os.File) error {
- header := make([]byte, 1+nonceV10Size)
- _, err := io.ReadFull(f, header)
- if err != nil {
- return err
- }
- h.version = header[0]
- if h.version == version10 {
- h.nonce = header[1:]
- return nil
- }
- return fmt.Errorf("unsupported encryption version: %v", h.version)
- }
- type cryptedFileWrapper struct {
- *os.File
- }
- func (w *cryptedFileWrapper) ReadAt(p []byte, offset int64) (n int, err error) {
- return w.File.ReadAt(p, offset+headerV10Size)
- }
|