dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit fd73a1d66de5f3d8b8951137c12cd301d9d7324c
parent 95ce7f88aa0bfe5b52c9e9c7313dcff07919f7c1
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Wed, 25 Jan 2023 21:22:05 -0800

ensure filedrops are encrypted on disk. Using stream encryption so it's efficient for big files

Diffstat:
Acmd/dkf/migrations/123.sql | 4++++
Mpkg/database/tableFiledrops.go | 18+++++++++---------
Apkg/utils/crypto/stream.go | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/utils/utils.go | 14++++++++++++++
Mpkg/web/handlers/handlers.go | 12+++++++++---
5 files changed, 176 insertions(+), 12 deletions(-)

diff --git a/cmd/dkf/migrations/123.sql b/cmd/dkf/migrations/123.sql @@ -0,0 +1,4 @@ +-- +migrate Up +ALTER TABLE filedrops ADD COLUMN iv BLOB NULL; + +-- +migrate Down diff --git a/pkg/database/tableFiledrops.go b/pkg/database/tableFiledrops.go @@ -4,7 +4,7 @@ import ( "dkforest/pkg/utils" "github.com/google/uuid" "github.com/sirupsen/logrus" - "io" + "io/ioutil" "os" "path/filepath" "time" @@ -18,6 +18,7 @@ type Filedrop struct { FileName string OrigFileName string FileSize int64 + IV []byte CreatedAt time.Time UpdatedAt *time.Time } @@ -55,23 +56,22 @@ func (d *Filedrop) GetContent() (os.FileInfo, []byte, error) { return nil, nil, err } defer f.Close() - - fileBytes, _ := io.ReadAll(f) - //decFileBytes, err := utils.DecryptAESMaster(fileBytes) - //if err != nil { - // decFileBytes = fileBytes - //} + decrypter, err := utils.DecryptStream(d.IV, f) + if err != nil { + return nil, nil, err + } + decFileBytes, _ := ioutil.ReadAll(decrypter) fi, err := f.Stat() if err != nil { return nil, nil, err } - return fi, fileBytes, nil + return fi, decFileBytes, nil } func (d *Filedrop) Delete() error { if d.FileName != "" { if err := os.Remove(filepath.Join(FiledropFolder, d.FileName)); err != nil { - return err + logrus.Error(err) } } if err := DB.Delete(&d).Error; err != nil { diff --git a/pkg/utils/crypto/stream.go b/pkg/utils/crypto/stream.go @@ -0,0 +1,140 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "errors" + "hash" + "io" +) + +// NewStreamEncrypter creates a new stream encrypter +func NewStreamEncrypter(encKey, macKey []byte, plainText io.Reader) (*StreamEncrypter, error) { + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + iv := make([]byte, block.BlockSize()) + _, err = rand.Read(iv) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(block, iv) + mac := hmac.New(sha256.New, macKey) + return &StreamEncrypter{ + Source: plainText, + Block: block, + Stream: stream, + Mac: mac, + IV: iv, + }, nil +} + +// NewStreamDecrypter creates a new stream decrypter +func NewStreamDecrypter(encKey, macKey []byte, meta StreamMeta, cipherText io.Reader) (*StreamDecrypter, error) { + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(block, meta.IV) + mac := hmac.New(sha256.New, macKey) + return &StreamDecrypter{ + Source: cipherText, + Block: block, + Stream: stream, + Mac: mac, + Meta: meta, + }, nil +} + +// StreamEncrypter is an encrypter for a stream of data with authentication +type StreamEncrypter struct { + Source io.Reader + Block cipher.Block + Stream cipher.Stream + Mac hash.Hash + IV []byte +} + +// StreamDecrypter is a decrypter for a stream of data with authentication +type StreamDecrypter struct { + Source io.Reader + Block cipher.Block + Stream cipher.Stream + Mac hash.Hash + Meta StreamMeta +} + +// Read encrypts the bytes of the inner reader and places them into p +func (s *StreamEncrypter) Read(p []byte) (int, error) { + n, readErr := s.Source.Read(p) + if n > 0 { + s.Stream.XORKeyStream(p[:n], p[:n]) + err := writeHash(s.Mac, p[:n]) + if err != nil { + return n, err + } + return n, readErr + } + return 0, io.EOF +} + +// Meta returns the encrypted stream metadata for use in decrypting. This should only be called after the stream is finished +func (s *StreamEncrypter) Meta() StreamMeta { + return StreamMeta{IV: s.IV, Hash: s.Mac.Sum(nil)} +} + +// Read reads bytes from the underlying reader and then decrypts them +func (s *StreamDecrypter) Read(p []byte) (int, error) { + n, readErr := s.Source.Read(p) + if n > 0 { + err := writeHash(s.Mac, p[:n]) + if err != nil { + return n, err + } + s.Stream.XORKeyStream(p[:n], p[:n]) + return n, readErr + } + return 0, io.EOF +} + +// Authenticate verifys that the hash of the stream is correct. This should only be called after processing is finished +func (s *StreamDecrypter) Authenticate() error { + if !hmac.Equal(s.Meta.Hash, s.Mac.Sum(nil)) { + return errors.New("authentication failed") + } + return nil +} + +func writeHash(mac hash.Hash, p []byte) error { + m, err := mac.Write(p) + if err != nil { + return err + } + if m != len(p) { + return errors.New("could not write all bytes to hmac") + } + return nil +} + +func checkedWrite(dst io.Writer, p []byte) (int, error) { + n, err := dst.Write(p) + if err != nil { + return n, err + } + if n != len(p) { + return n, errors.New("unable to write all bytes") + } + return len(p), nil +} + +// StreamMeta is metadata about an encrypted stream +type StreamMeta struct { + // IV is the initial value for the crypto function + IV []byte + // Hash is the sha256 hmac of the stream + Hash []byte +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go @@ -13,6 +13,7 @@ import ( "crypto/x509" "dkforest/pkg/bfchroma" bf "dkforest/pkg/blackfriday/v2" + "dkforest/pkg/utils/crypto" "encoding/binary" "encoding/hex" "encoding/json" @@ -367,6 +368,19 @@ func DecryptAESMaster(ciphertext []byte) ([]byte, error) { return DecryptAES(ciphertext, []byte(config.Global.MasterKey())) } +func EncryptStream(src io.Reader) (*crypto.StreamEncrypter, error) { + return crypto.NewStreamEncrypter([]byte(config.Global.MasterKey()), nil, src) +} + +func DecryptStream(iv []byte, src io.Reader) (*crypto.StreamDecrypter, error) { + encKey := []byte(config.Global.MasterKey()) + decrypter, err := crypto.NewStreamDecrypter(encKey, nil, crypto.StreamMeta{IV: iv}, src) + if err != nil { + return nil, err + } + return decrypter, nil +} + func GetGCM(key string) (cipher.AEAD, int, error) { keyBytes, err := hex.DecodeString(key) if err != nil { diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -4198,18 +4198,24 @@ func FileDropHandler(c echo.Context) error { } newFileName := utils.MD5([]byte(utils.GenerateToken32())) - f, err := os.OpenFile(filepath.Join(database.FiledropFolder, newFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + outFile, err := os.OpenFile(filepath.Join(database.FiledropFolder, newFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { data.Error = err.Error() return c.HTML(http.StatusOK, formHTML+data.Error) } - defer f.Close() - written, err := io.Copy(f, file) + defer outFile.Close() + encrypter, err := utils.EncryptStream(file) + if err != nil { + data.Error = err.Error() + return c.HTML(http.StatusOK, formHTML+data.Error) + } + written, err := io.Copy(outFile, encrypter) if err != nil { data.Error = err.Error() return c.HTML(http.StatusOK, formHTML+data.Error) } + filedrop.IV = encrypter.Meta().IV filedrop.FileName = newFileName filedrop.OrigFileName = origFileName filedrop.FileSize = written