mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-29 02:50:29 +00:00
a6985075b9
Fixes #224
350 lines
7.8 KiB
Go
350 lines
7.8 KiB
Go
package vfs
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/eikenb/pipeat"
|
|
"github.com/minio/sha256-simd"
|
|
"github.com/minio/sio"
|
|
"golang.org/x/crypto/hkdf"
|
|
|
|
"github.com/drakkan/sftpgo/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
|
|
masterKey []byte
|
|
}
|
|
|
|
// NewCryptFs returns a CryptFs object
|
|
func NewCryptFs(connectionID, rootDir string, config CryptFsConfig) (Fs, error) {
|
|
if err := config.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.Passphrase.IsEncrypted() {
|
|
if err := config.Passphrase.Decrypt(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
fs := &CryptFs{
|
|
OsFs: &OsFs{
|
|
name: cryptFsName,
|
|
connectionID: connectionID,
|
|
rootDir: rootDir,
|
|
virtualFolders: nil,
|
|
},
|
|
masterKey: []byte(config.Passphrase.GetPayload()),
|
|
}
|
|
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.rootDir)
|
|
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: %#v", 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)
|
|
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: %#v 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, os.ModePerm)
|
|
}
|
|
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.rootDir)
|
|
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))
|
|
f.Close()
|
|
r.CloseWithError(err) //nolint:errcheck
|
|
p.Done(err)
|
|
fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %v", name, n, err)
|
|
}()
|
|
|
|
return nil, p, nil, nil
|
|
}
|
|
|
|
// Truncate changes the size of the named file
|
|
func (*CryptFs) Truncate(name string, size 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
|
|
}
|
|
|
|
// IsAtomicUploadSupported returns true if atomic upload is supported
|
|
func (*CryptFs) IsAtomicUploadSupported() 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)
|
|
}
|