mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 13:55:21 +02:00
fix(encryption): replace encryption related methods for fips mode [be-11933] (#919)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
This commit is contained in:
parent
163aa57e5c
commit
d306d7a983
19 changed files with 701 additions and 169 deletions
|
@ -9,10 +9,15 @@ import (
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fips.InitFIPS(false)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPingAgentPanic(t *testing.T) {
|
func TestPingAgentPanic(t *testing.T) {
|
||||||
endpoint := &portainer.Endpoint{
|
endpoint := &portainer.Endpoint{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
|
|
|
@ -49,6 +49,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/pkg/build"
|
"github.com/portainer/portainer/pkg/build"
|
||||||
"github.com/portainer/portainer/pkg/featureflags"
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
"github.com/portainer/portainer/pkg/libhelm"
|
"github.com/portainer/portainer/pkg/libhelm"
|
||||||
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
|
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
|
||||||
"github.com/portainer/portainer/pkg/libstack/compose"
|
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||||
|
@ -343,6 +344,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -ce can not ever be run in FIPS mode
|
||||||
|
fips.InitFIPS(false)
|
||||||
|
|
||||||
fileService := initFileService(*flags.Data)
|
fileService := initFileService(*flags.Data)
|
||||||
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
||||||
if encryptionKey == nil {
|
if encryptionKey == nil {
|
||||||
|
|
|
@ -6,11 +6,15 @@ import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
"golang.org/x/crypto/scrypt"
|
"golang.org/x/crypto/scrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,20 +23,32 @@ const (
|
||||||
aesGcmHeader = "AES256-GCM" // The encrypted file header
|
aesGcmHeader = "AES256-GCM" // The encrypted file header
|
||||||
aesGcmBlockSize = 1024 * 1024 // 1MB block for aes gcm
|
aesGcmBlockSize = 1024 * 1024 // 1MB block for aes gcm
|
||||||
|
|
||||||
|
aesGcmFIPSHeader = "FIPS-AES256-GCM"
|
||||||
|
aesGcmFIPSBlockSize = 16 * 1024 * 1024 // 16MB block for aes gcm
|
||||||
|
|
||||||
// Argon2 settings
|
// Argon2 settings
|
||||||
// Recommded settings lower memory hardware according to current OWASP recommendations
|
// Recommended 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
|
// 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
|
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
||||||
argon2MemoryCost = 12 * 1024
|
argon2MemoryCost = 12 * 1024
|
||||||
argon2TimeCost = 3
|
argon2TimeCost = 3
|
||||||
argon2Threads = 1
|
argon2Threads = 1
|
||||||
argon2KeyLength = 32
|
argon2KeyLength = 32
|
||||||
|
|
||||||
|
pbkdf2Iterations = 600_000 // use recommended iterations from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 a little overkill for this use
|
||||||
|
pbkdf2SaltLength = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
|
// 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 {
|
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||||
if err := aesEncryptGCM(input, output, passphrase); err != nil {
|
if fips.FIPSMode() {
|
||||||
return fmt.Errorf("error encrypting file: %w", err)
|
if err := aesEncryptGCMFIPS(input, output, passphrase); err != nil {
|
||||||
|
return fmt.Errorf("error encrypting file: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := aesEncryptGCM(input, output, passphrase); err != nil {
|
||||||
|
return fmt.Errorf("error encrypting file: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -40,14 +56,36 @@ func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||||
|
|
||||||
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from
|
// 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) {
|
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
fipsMode := fips.FIPSMode()
|
||||||
|
return aesDecrypt(input, passphrase, fipsMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aesDecrypt(input io.Reader, passphrase []byte, fipsMode bool) (io.Reader, error) {
|
||||||
// Read file header to determine how it was encrypted
|
// Read file header to determine how it was encrypted
|
||||||
inputReader := bufio.NewReader(input)
|
inputReader := bufio.NewReader(input)
|
||||||
header, err := inputReader.Peek(len(aesGcmHeader))
|
header, err := inputReader.Peek(len(aesGcmFIPSHeader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error reading encrypted backup file header: %w", err)
|
return nil, fmt.Errorf("error reading encrypted backup file header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(header) == aesGcmHeader {
|
if strings.HasPrefix(string(header), aesGcmFIPSHeader) {
|
||||||
|
if !fipsMode {
|
||||||
|
return nil, errors.New("fips encrypted file detected but fips mode is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := aesDecryptGCMFIPS(inputReader, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decrypting file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(string(header), aesGcmHeader) {
|
||||||
|
if fipsMode {
|
||||||
|
return nil, errors.New("fips mode is enabled but non-fips encrypted file detected")
|
||||||
|
}
|
||||||
|
|
||||||
reader, err := aesDecryptGCM(inputReader, passphrase)
|
reader, err := aesDecryptGCM(inputReader, passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error decrypting file: %w", err)
|
return nil, fmt.Errorf("error decrypting file: %w", err)
|
||||||
|
@ -203,6 +241,126 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
return &buf, nil
|
return &buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aesEncryptGCMFIPS reads from input, encrypts with AES-256 in a fips compliant
|
||||||
|
// way and writes to output. passphrase is used to generate an encryption key.
|
||||||
|
func aesEncryptGCMFIPS(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||||
|
salt := make([]byte, pbkdf2SaltLength)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := pbkdf2.Key(passphrase, salt, pbkdf2Iterations, 32, sha256.New)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the header
|
||||||
|
if _, err := output.Write([]byte(aesGcmFIPSHeader)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write nonce and salt to the output file
|
||||||
|
if _, err := output.Write(salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer for reading plaintext blocks
|
||||||
|
buf := make([]byte, aesGcmFIPSBlockSize)
|
||||||
|
|
||||||
|
// Encrypt plaintext in blocks
|
||||||
|
for {
|
||||||
|
// new random nonce for each block
|
||||||
|
aesgcm, err := cipher.NewGCMWithRandomNonce(block)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating gcm: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
ciphertext := aesgcm.Seal(nil, nil, buf[:n], nil)
|
||||||
|
|
||||||
|
_, err = output.Write(ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// aesDecryptGCMFIPS reads from input, decrypts with AES-256 in a fips compliant
|
||||||
|
// way and returns the reader to read the decrypted content from.
|
||||||
|
func aesDecryptGCMFIPS(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
// Reader & verify header
|
||||||
|
header := make([]byte, len(aesGcmFIPSHeader))
|
||||||
|
if _, err := io.ReadFull(input, header); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(header) != aesGcmFIPSHeader {
|
||||||
|
return nil, errors.New("invalid header")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read salt
|
||||||
|
salt := make([]byte, pbkdf2SaltLength)
|
||||||
|
if _, err := io.ReadFull(input, salt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := pbkdf2.Key(passphrase, salt, pbkdf2Iterations, 32, sha256.New)
|
||||||
|
|
||||||
|
// Initialize AES cipher block
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a buffer to store decrypted data
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
|
||||||
|
// Decrypt the ciphertext in blocks
|
||||||
|
for {
|
||||||
|
// Create GCM mode with the cipher block
|
||||||
|
aesgcm, err := cipher.NewGCMWithRandomNonce(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a block of ciphertext from the input reader
|
||||||
|
ciphertextBlock := make([]byte, aesGcmFIPSBlockSize+aesgcm.Overhead())
|
||||||
|
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(nil, nil, ciphertextBlock[:n], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := buf.Write(plaintext); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
// aesDecryptOFB reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
|
// 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.
|
// 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
|
// note: This function used to decrypt files that were encrypted without a header i.e. old archives
|
||||||
|
|
|
@ -7,9 +7,15 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fips.InitFIPS(false)
|
||||||
|
}
|
||||||
|
|
||||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
func randBytes(n int) []byte {
|
func randBytes(n int) []byte {
|
||||||
|
@ -20,198 +26,296 @@ func randBytes(n int) []byte {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type encryptFunc func(input io.Reader, output io.Writer, passphrase []byte) error
|
||||||
|
type decryptFunc func(input io.Reader, passphrase []byte) (io.Reader, error)
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||||
const passphrase = "passphrase"
|
const passphrase = "passphrase"
|
||||||
|
|
||||||
tmpdir := t.TempDir()
|
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc, decryptShouldSucceed bool) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin")
|
originFilePath = filepath.Join(tmpdir, "origin")
|
||||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
|
||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := randBytes(1024*1024*100 + 523)
|
content := randBytes(1024*1024*100 + 523)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
|
|
||||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
err := encrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
require.Nil(t, err, "Failed to encrypt a file")
|
||||||
encryptedFileWriter.Close()
|
encryptedFileWriter.Close()
|
||||||
|
|
||||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
require.Nil(t, err, "Couldn't read encrypted file")
|
||||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
defer encryptedFileReader.Close()
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
defer decryptedFileWriter.Close()
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
|
decryptedReader, err := decrypt(encryptedFileReader, []byte(passphrase))
|
||||||
assert.Nil(t, err, "Failed to decrypt file")
|
if !decryptShouldSucceed {
|
||||||
|
require.Error(t, err, "Failed to decrypt file as indicated by decryptShouldSucceed")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err, "Failed to decrypt file indicated by decryptShouldSucceed")
|
||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, aesDecryptGCMFIPS, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCM, aesDecryptGCM, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("system_fips_mode_public_entry_points", func(t *testing.T) {
|
||||||
|
// use the init mode, public entry points
|
||||||
|
testFunc(t, AesEncrypt, AesDecrypt, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fips_encrypted_file_header_fails_in_non_fips_mode", func(t *testing.T) {
|
||||||
|
// use aesDecrypt which checks the header, confirm that it fails
|
||||||
|
decrypt := func(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
return aesDecrypt(input, passphrase, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, decrypt, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips_encrypted_file_header_fails_in_fips_mode", func(t *testing.T) {
|
||||||
|
// use aesDecrypt which checks the header, confirm that it fails
|
||||||
|
decrypt := func(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
return aesDecrypt(input, passphrase, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFunc(t, aesEncryptGCM, decrypt, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fips_encrypted_file_fails_in_non_fips_mode", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, aesDecryptGCM, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips_encrypted_file_with_fips_mode_should_fail", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCM, aesDecryptGCMFIPS, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fips_with_base_aesDecrypt", func(t *testing.T) {
|
||||||
|
// maximize coverage, use the base aesDecrypt function with valid fips mode
|
||||||
|
decrypt := func(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
return aesDecrypt(input, passphrase, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, decrypt, true)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
|
func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
|
||||||
const passphrase = "A strong passphrase with special characters: !@#$%^&*()_+"
|
const passphrase = "A strong passphrase with special characters: !@#$%^&*()_+"
|
||||||
tmpdir := t.TempDir()
|
|
||||||
|
|
||||||
var (
|
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
|
||||||
originFilePath = filepath.Join(tmpdir, "origin2")
|
tmpdir := t.TempDir()
|
||||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
|
||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
|
||||||
)
|
|
||||||
|
|
||||||
content := randBytes(500)
|
var (
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
originFilePath = filepath.Join(tmpdir, "origin2")
|
||||||
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
||||||
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
||||||
|
)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
content := randBytes(500)
|
||||||
defer originFile.Close()
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
|
defer originFile.Close()
|
||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
|
||||||
encryptedFileWriter.Close()
|
|
||||||
|
|
||||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
err := encrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
encryptedFileWriter.Close()
|
||||||
|
|
||||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
defer encryptedFileReader.Close()
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
defer decryptedFileWriter.Close()
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
assert.Nil(t, err, "Failed to decrypt file")
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
decryptedReader, err := decrypt(encryptedFileReader, []byte(passphrase))
|
||||||
|
assert.Nil(t, err, "Failed to decrypt file")
|
||||||
|
|
||||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
|
||||||
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, aesDecryptGCMFIPS)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCM, aesDecryptGCM)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
|
func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
|
||||||
tmpdir := t.TempDir()
|
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin2")
|
originFilePath = filepath.Join(tmpdir, "origin2")
|
||||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := randBytes(500)
|
content := randBytes(500)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
|
|
||||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
err := encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
encryptedFileWriter.Close()
|
encryptedFileWriter.Close()
|
||||||
|
|
||||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
defer encryptedFileReader.Close()
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
defer decryptedFileWriter.Close()
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("passphrase"))
|
decryptedReader, err := decrypt(encryptedFileReader, []byte("passphrase"))
|
||||||
assert.Nil(t, err, "Failed to decrypt file")
|
assert.Nil(t, err, "Failed to decrypt file")
|
||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, aesDecryptGCMFIPS)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCM, aesDecryptGCM)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||||
tmpdir := t.TempDir()
|
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin")
|
originFilePath = filepath.Join(tmpdir, "origin")
|
||||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
|
||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := randBytes(1024 * 50)
|
content := randBytes(1024 * 50)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
|
|
||||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
defer encryptedFileWriter.Close()
|
defer encryptedFileWriter.Close()
|
||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
|
err := encrypt(originFile, encryptedFileWriter, []byte(""))
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
defer encryptedFileReader.Close()
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
defer decryptedFileWriter.Close()
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(""))
|
decryptedReader, err := decrypt(encryptedFileReader, []byte(""))
|
||||||
assert.Nil(t, err, "Failed to decrypt file")
|
assert.Nil(t, err, "Failed to decrypt file")
|
||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, aesDecryptGCMFIPS)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCM, aesDecryptGCM)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
|
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
|
||||||
tmpdir := t.TempDir()
|
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin")
|
originFilePath = filepath.Join(tmpdir, "origin")
|
||||||
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
|
||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := randBytes(1034)
|
content := randBytes(1034)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
|
|
||||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
defer encryptedFileWriter.Close()
|
defer encryptedFileWriter.Close()
|
||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
err := encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
defer encryptedFileReader.Close()
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
defer decryptedFileWriter.Close()
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
_, err = AesDecrypt(encryptedFileReader, []byte("garbage"))
|
_, err = decrypt(encryptedFileReader, []byte("garbage"))
|
||||||
assert.NotNil(t, err, "Should not allow decrypt with wrong passphrase")
|
assert.NotNil(t, err, "Should not allow decrypt with wrong passphrase")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCMFIPS, aesDecryptGCMFIPS)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_fips", func(t *testing.T) {
|
||||||
|
testFunc(t, aesEncryptGCM, aesDecryptGCM)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||||
message = service.secret
|
message = service.secret
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := libcrypto.HashFromBytes([]byte(message))
|
hash := libcrypto.InsecureHashFromBytes([]byte(message))
|
||||||
|
|
||||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
22
api/crypto/ecdsa_test.go
Normal file
22
api/crypto/ecdsa_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateSignature(t *testing.T) {
|
||||||
|
var s = NewECDSAService("secret")
|
||||||
|
|
||||||
|
privKey, pubKey, err := s.GenerateKeyPair()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Greater(t, len(privKey), 0)
|
||||||
|
require.Greater(t, len(pubKey), 0)
|
||||||
|
|
||||||
|
m := "test message"
|
||||||
|
r, err := s.CreateSignature(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, r, m)
|
||||||
|
require.Greater(t, len(r), 0)
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ func NewNonce(size int) *Nonce {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRandomNonce generates a new initial nonce with the lower byte set to a random value
|
// NewRandomNonce generates a new initial nonce with the lower byte set to a random value
|
||||||
// This ensures there are plenty of nonce values availble before rolling over
|
// This ensures there are plenty of nonce values available before rolling over
|
||||||
// Based on ideas from the Secure Programming Cookbook for C and C++ by John Viega, Matt Messier
|
// Based on ideas from the Secure Programming Cookbook for C and C++ by John Viega, Matt Messier
|
||||||
// https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch04s09.html
|
// https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch04s09.html
|
||||||
func NewRandomNonce(size int) (*Nonce, error) {
|
func NewRandomNonce(size int) (*Nonce, error) {
|
||||||
|
|
|
@ -4,8 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/rand"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
|
@ -65,18 +63,18 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
|
||||||
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
|
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
|
||||||
|
|
||||||
func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
|
func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
|
||||||
block, _ := aes.NewCipher(passphrase)
|
block, err := aes.NewCipher(passphrase)
|
||||||
gcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return encrypted, err
|
return encrypted, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
// NewGCMWithRandomNonce in go 1.24 handles setting up the nonce and adding it to the encrypted output
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
gcm, err := cipher.NewGCMWithRandomNonce(block)
|
||||||
|
if err != nil {
|
||||||
return encrypted, err
|
return encrypted, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
return gcm.Seal(nil, nil, plaintext, nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
|
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
|
||||||
|
@ -89,19 +87,17 @@ func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err err
|
||||||
return encrypted, errors.Wrap(err, "Error creating cypher block")
|
return encrypted, errors.Wrap(err, "Error creating cypher block")
|
||||||
}
|
}
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
// NewGCMWithRandomNonce in go 1.24 handles reading the nonce from the encrypted input for us
|
||||||
|
gcm, err := cipher.NewGCMWithRandomNonce(block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return encrypted, errors.Wrap(err, "Error creating GCM")
|
return encrypted, errors.Wrap(err, "Error creating GCM")
|
||||||
}
|
}
|
||||||
|
|
||||||
nonceSize := gcm.NonceSize()
|
if len(encrypted) < gcm.NonceSize() {
|
||||||
if len(encrypted) < nonceSize {
|
|
||||||
return encrypted, errEncryptedStringTooShort
|
return encrypted, errEncryptedStringTooShort
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
|
plaintextByte, err = gcm.Open(nil, nil, encrypted, nil)
|
||||||
|
|
||||||
plaintextByte, err = gcm.Open(nil, nonce, ciphertextByteClean, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return encrypted, errors.Wrap(err, "Error decrypting text")
|
return encrypted, errors.Wrap(err, "Error decrypting text")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
package boltdb
|
package boltdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -160,7 +167,7 @@ func Test_ObjectMarshallingEncrypted(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
key := secretToEncryptionKey(passphrase)
|
key := secretToEncryptionKey(passphrase)
|
||||||
conn := DbConnection{EncryptionKey: key}
|
conn := DbConnection{EncryptionKey: key, isEncrypted: true}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
|
||||||
|
|
||||||
|
@ -175,3 +182,94 @@ func Test_ObjectMarshallingEncrypted(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_NonceSources(t *testing.T) {
|
||||||
|
// ensure that the new go 1.24 NewGCMWithRandomNonce works correctly with
|
||||||
|
// the old way of creating and including the nonce
|
||||||
|
|
||||||
|
encryptOldFn := func(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
|
||||||
|
block, _ := aes.NewCipher(passphrase)
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return encrypted, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return encrypted, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptOldFn := func(encrypted []byte, passphrase []byte) (plaintext []byte, err error) {
|
||||||
|
block, err := aes.NewCipher(passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return encrypted, errors.Wrap(err, "Error creating cypher block")
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return encrypted, errors.Wrap(err, "Error creating GCM")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(encrypted) < nonceSize {
|
||||||
|
return encrypted, errEncryptedStringTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
|
||||||
|
|
||||||
|
plaintext, err = gcm.Open(nil, nonce, ciphertextByteClean, nil)
|
||||||
|
if err != nil {
|
||||||
|
return encrypted, errors.Wrap(err, "Error decrypting text")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptNewFn := encrypt
|
||||||
|
decryptNewFn := decrypt
|
||||||
|
|
||||||
|
passphrase := make([]byte, 32)
|
||||||
|
_, err := io.ReadFull(rand.Reader, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
junk := make([]byte, 1024)
|
||||||
|
_, err = io.ReadFull(rand.Reader, junk)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
junkEnc := make([]byte, base64.StdEncoding.EncodedLen(len(junk)))
|
||||||
|
base64.StdEncoding.Encode(junkEnc, junk)
|
||||||
|
|
||||||
|
cases := [][]byte{
|
||||||
|
[]byte("test"),
|
||||||
|
[]byte("35"),
|
||||||
|
[]byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
|
||||||
|
[]byte(jsonobject),
|
||||||
|
passphrase,
|
||||||
|
junk,
|
||||||
|
junkEnc,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, plain := range cases {
|
||||||
|
var enc, dec []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
enc, err = encryptOldFn(plain, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dec, err = decryptNewFn(enc, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, plain, dec)
|
||||||
|
|
||||||
|
enc, err = encryptNewFn(plain, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dec, err = decryptOldFn(enc, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, plain, dec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,10 +18,15 @@ import (
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/http/offlinegate"
|
"github.com/portainer/portainer/api/http/offlinegate"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fips.InitFIPS(false)
|
||||||
|
}
|
||||||
|
|
||||||
func listFiles(dir string) []string {
|
func listFiles(dir string) []string {
|
||||||
items := make([]string, 0)
|
items := make([]string, 0)
|
||||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
|
|
@ -20,12 +20,17 @@ import (
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fips.InitFIPS(false)
|
||||||
|
}
|
||||||
|
|
||||||
var testFileContent = "abcdefg"
|
var testFileContent = "abcdefg"
|
||||||
|
|
||||||
type TestGitService struct {
|
type TestGitService struct {
|
||||||
|
|
|
@ -60,7 +60,7 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
message := strings.Join(data.Message, "\n")
|
message := strings.Join(data.Message, "\n")
|
||||||
|
|
||||||
hash := libcrypto.HashFromBytes([]byte(message))
|
hash := libcrypto.InsecureHashFromBytes([]byte(message))
|
||||||
resp := motdResponse{
|
resp := motdResponse{
|
||||||
Title: data.Title,
|
Title: data.Title,
|
||||||
Message: message,
|
Message: message,
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -236,6 +236,7 @@ require (
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/portainer/agent v0.0.0-20250713222305-88cd348ec200 // indirect
|
||||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
|
|
39
pkg/fips/fips.go
Normal file
39
pkg/fips/fips.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package fips
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/fips140"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fipsMode, isInitialised bool
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func InitFIPS(enabled bool) {
|
||||||
|
once.Do(func() {
|
||||||
|
isInitialised = true
|
||||||
|
fipsMode = enabled
|
||||||
|
|
||||||
|
if enabled && !fips140.Enabled() {
|
||||||
|
log.Fatal().Msg("If FIPS mode is enabled then the fips140 GODEBUG environment variable must be set")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FIPSMode() bool {
|
||||||
|
if !isInitialised {
|
||||||
|
log.Fatal().Msg("Could not determine if FIPS mode is enabled because InitFIPS was never called")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fipsMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func CanTLSSkipVerify() bool {
|
||||||
|
if !isInitialised {
|
||||||
|
log.Fatal().Msg("Could not determine if FIPS mode is enabled because InitFIPS was never called")
|
||||||
|
}
|
||||||
|
|
||||||
|
return !fipsMode
|
||||||
|
}
|
15
pkg/fips/fips_test.go
Normal file
15
pkg/fips/fips_test.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package fips
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitFIPS(t *testing.T) {
|
||||||
|
InitFIPS(false)
|
||||||
|
|
||||||
|
require.False(t, FIPSMode())
|
||||||
|
|
||||||
|
require.True(t, CanTLSSkipVerify())
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of
|
// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of
|
||||||
|
@ -11,14 +13,25 @@ import (
|
||||||
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||||
// Creates a 32bit hash of the key before decrypting the data.
|
// Creates a 32bit hash of the key before decrypting the data.
|
||||||
func Decrypt(data []byte, key []byte) ([]byte, error) {
|
func Decrypt(data []byte, key []byte) ([]byte, error) {
|
||||||
hashKey := Hash32Bit(key)
|
return decrypt(data, key, fips.FIPSMode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(data []byte, key []byte, fips bool) ([]byte, error) {
|
||||||
|
var hashKey []byte
|
||||||
|
if fips {
|
||||||
|
// sha256 hash 32 bytes
|
||||||
|
hashKey = HashFromBytes(key)
|
||||||
|
} else {
|
||||||
|
// 16 byte hash, hex encoded is 32 bytes
|
||||||
|
hashKey = InsecureHash32Bytes(key)
|
||||||
|
}
|
||||||
|
|
||||||
block, err := aes.NewCipher(hashKey)
|
block, err := aes.NewCipher(hashKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
gcm, err := cipher.NewGCMWithRandomNonce(block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -28,8 +41,8 @@ func Decrypt(data []byte, key []byte) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return gcm.Open(nil,
|
return gcm.Open(nil,
|
||||||
data[:gcm.NonceSize()],
|
nil,
|
||||||
data[gcm.NonceSize():],
|
data,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package libcrypto
|
||||||
import (
|
import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/rand"
|
|
||||||
"io"
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of
|
// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of
|
||||||
|
@ -12,23 +12,26 @@ import (
|
||||||
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||||
// Creates a 32bit hash of the key before encrypting the data.
|
// Creates a 32bit hash of the key before encrypting the data.
|
||||||
func Encrypt(data, key []byte) ([]byte, error) {
|
func Encrypt(data, key []byte) ([]byte, error) {
|
||||||
hashKey := Hash32Bit(key)
|
return encrypt(data, key, fips.FIPSMode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func encrypt(data, key []byte, fips bool) ([]byte, error) {
|
||||||
|
var hashKey []byte
|
||||||
|
if fips {
|
||||||
|
hashKey = HashFromBytes(key)
|
||||||
|
} else {
|
||||||
|
hashKey = InsecureHash32Bytes(key)
|
||||||
|
}
|
||||||
|
|
||||||
block, err := aes.NewCipher(hashKey)
|
block, err := aes.NewCipher(hashKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
gcm, err := cipher.NewGCMWithRandomNonce(block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
return gcm.Seal(nil, nil, data, nil), nil
|
||||||
_, err = io.ReadFull(rand.Reader, nonce)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
|
||||||
}
|
}
|
||||||
|
|
56
pkg/libcrypto/encrypt_test.go
Normal file
56
pkg/libcrypto/encrypt_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package libcrypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fips.InitFIPS(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt(t *testing.T) {
|
||||||
|
dataFn := func() []byte {
|
||||||
|
data := make([]byte, 1024)
|
||||||
|
_, err := io.ReadFull(rand.Reader, data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := func(t *testing.T, fips bool) {
|
||||||
|
data := dataFn()
|
||||||
|
|
||||||
|
encrypted, err := encrypt(data, []byte("test"), fips)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := decrypt(encrypted, []byte("test"), fips)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, data, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("fips", func(t *testing.T) {
|
||||||
|
fn(t, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-fips", func(t *testing.T) {
|
||||||
|
fn(t, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("system_fips_mode", func(t *testing.T) {
|
||||||
|
data := dataFn()
|
||||||
|
|
||||||
|
encrypted, err := Encrypt(data, []byte("test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(encrypted, []byte("test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, data, decrypted)
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,18 +2,26 @@ package libcrypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HashFromBytes returns the hash of the specified data
|
// InsecureHashFromBytes returns the 16 byte md5 hash of the specified data
|
||||||
func HashFromBytes(data []byte) []byte {
|
func InsecureHashFromBytes(data []byte) []byte {
|
||||||
digest := md5.New()
|
digest := md5.New()
|
||||||
digest.Write(data)
|
digest.Write(data)
|
||||||
return digest.Sum(nil)
|
return digest.Sum(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash32Bit returns a hexadecimal encoded hash
|
// InsecureHash32Bytes returns a hexadecimal encoded hash to make a 16 byte md5 hash into 32 bytes
|
||||||
func Hash32Bit(data []byte) []byte {
|
func InsecureHash32Bytes(data []byte) []byte {
|
||||||
hash := HashFromBytes(data)
|
hash := InsecureHashFromBytes(data)
|
||||||
return []byte(hex.EncodeToString(hash))
|
return []byte(hex.EncodeToString(hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashFromBytes returns the 32 byte sha256 hash of the specified data
|
||||||
|
func HashFromBytes(data []byte) []byte {
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write(data)
|
||||||
|
return hash.Sum(nil)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue