mirror of https://github.com/portainer/portainer
233 lines
6.2 KiB
Go
233 lines
6.2 KiB
Go
package crypto
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
"golang.org/x/crypto/scrypt"
|
|
)
|
|
|
|
const (
|
|
// AES GCM settings
|
|
aesGcmHeader = "AES256-GCM" // The encrypted file header
|
|
aesGcmBlockSize = 1024 * 1024 // 1MB block for aes gcm
|
|
|
|
// Argon2 settings
|
|
// Recommded settings lower memory hardware according to current OWASP recommendations
|
|
// Considering some people run portainer on a NAS I think it's prudent not to assume we're on server grade hardware
|
|
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
|
argon2MemoryCost = 12 * 1024
|
|
argon2TimeCost = 3
|
|
argon2Threads = 1
|
|
argon2KeyLength = 32
|
|
)
|
|
|
|
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
|
|
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
|
|
err := aesEncryptGCM(input, output, passphrase)
|
|
if err != nil {
|
|
return fmt.Errorf("error encrypting file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from
|
|
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
|
// Read file header to determine how it was encrypted
|
|
inputReader := bufio.NewReader(input)
|
|
header, err := inputReader.Peek(len(aesGcmHeader))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading encrypted backup file header: %w", err)
|
|
}
|
|
|
|
if string(header) == aesGcmHeader {
|
|
reader, err := aesDecryptGCM(inputReader, passphrase)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decrypting file: %w", err)
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
// Use the previous decryption routine which has no header (to support older archives)
|
|
reader, err := aesDecryptOFB(inputReader, passphrase)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decrypting legacy file backup: %w", err)
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
// aesEncryptGCM reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key.
|
|
func aesEncryptGCM(input io.Reader, output io.Writer, passphrase []byte) error {
|
|
// Derive key using argon2 with a random salt
|
|
salt := make([]byte, 16) // 16 bytes salt
|
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
|
return err
|
|
}
|
|
|
|
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Generate nonce
|
|
nonce, err := NewRandomNonce(aesgcm.NonceSize())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// write the header
|
|
if _, err := output.Write([]byte(aesGcmHeader)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write nonce and salt to the output file
|
|
if _, err := output.Write(salt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := output.Write(nonce.Value()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Buffer for reading plaintext blocks
|
|
buf := make([]byte, aesGcmBlockSize) // Adjust buffer size as needed
|
|
ciphertext := make([]byte, len(buf)+aesgcm.Overhead())
|
|
|
|
// Encrypt plaintext in blocks
|
|
for {
|
|
n, err := io.ReadFull(input, buf)
|
|
if n == 0 {
|
|
break // end of plaintext input
|
|
}
|
|
|
|
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
|
|
return err
|
|
}
|
|
|
|
// Seal encrypts the plaintext using the nonce returning the updated slice.
|
|
ciphertext = aesgcm.Seal(ciphertext[:0], nonce.Value(), buf[:n], nil)
|
|
|
|
_, err = output.Write(ciphertext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nonce.Increment()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// aesDecryptGCM reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from.
|
|
func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
|
|
// Reader & verify header
|
|
header := make([]byte, len(aesGcmHeader))
|
|
if _, err := io.ReadFull(input, header); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if string(header) != aesGcmHeader {
|
|
return nil, fmt.Errorf("invalid header")
|
|
}
|
|
|
|
// Read salt
|
|
salt := make([]byte, 16) // Salt size
|
|
if _, err := io.ReadFull(input, salt); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
|
|
|
|
// Initialize AES cipher block
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create GCM mode with the cipher block
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read nonce from the input reader
|
|
nonce := NewNonce(aesgcm.NonceSize())
|
|
if err := nonce.Read(input); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize a buffer to store decrypted data
|
|
buf := bytes.Buffer{}
|
|
plaintext := make([]byte, aesGcmBlockSize)
|
|
|
|
// Decrypt the ciphertext in blocks
|
|
for {
|
|
// Read a block of ciphertext from the input reader
|
|
ciphertextBlock := make([]byte, aesGcmBlockSize+aesgcm.Overhead()) // Adjust block size as needed
|
|
n, err := io.ReadFull(input, ciphertextBlock)
|
|
if n == 0 {
|
|
break // end of ciphertext
|
|
}
|
|
|
|
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
|
|
return nil, err
|
|
}
|
|
|
|
// Decrypt the block of ciphertext
|
|
plaintext, err = aesgcm.Open(plaintext[:0], nonce.Value(), ciphertextBlock[:n], nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = buf.Write(plaintext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonce.Increment()
|
|
}
|
|
|
|
return &buf, nil
|
|
}
|
|
|
|
// aesDecryptOFB reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
|
|
// passphrase is used to generate an encryption key.
|
|
// note: This function used to decrypt files that were encrypted without a header i.e. old archives
|
|
func aesDecryptOFB(input io.Reader, passphrase []byte) (io.Reader, error) {
|
|
var emptySalt []byte = make([]byte, 0)
|
|
|
|
// making a 32 bytes key that would correspond to AES-256
|
|
// don't necessarily need a salt, so just kept in empty
|
|
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the key is unique for each ciphertext, then it's ok to use a zero IV.
|
|
var iv [aes.BlockSize]byte
|
|
stream := cipher.NewOFB(block, iv[:])
|
|
reader := &cipher.StreamReader{S: stream, R: input}
|
|
|
|
return reader, nil
|
|
}
|