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:
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