Files
probo/pkg/crypto/cipher/cipher.go
Bryan Frimin 44a05be1af Encrypt connector sensitive data
Add database field level encryption level to sensitive data to reduce
the risk in term of data leak. I dedice to have only one key for now in
a near future I may move to one master key and one encryption key per
organization to make rotation easiest.

I don't use built-in pg_crypto function to have clear seperation and
avoid any encryption key leak.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2025-04-20 21:16:18 -07:00

117 lines
2.8 KiB
Go

// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cipher
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
)
type (
EncryptionKey [32]byte
)
func NewEncryptionKey(key string) (EncryptionKey, error) {
if len(key) != 32 {
return EncryptionKey{}, fmt.Errorf("key must be 32 bytes for AES-256")
}
var encryptionKey EncryptionKey
copy(encryptionKey[:], key)
return encryptionKey, nil
}
func (k EncryptionKey) Bytes() []byte {
return k[:]
}
func (k *EncryptionKey) UnmarshalJSON(data []byte) error {
var key string
if err := json.Unmarshal(data, &key); err != nil {
return err
}
return k.UnmarshalText([]byte(key))
}
func (k *EncryptionKey) UnmarshalText(text []byte) error {
decoded, err := base64.StdEncoding.DecodeString(string(text))
if err != nil {
return fmt.Errorf("cannot decode base64 key: %w", err)
}
if len(decoded) != 32 {
return fmt.Errorf("key must be 32 bytes for AES-256, got %d bytes after base64 decoding", len(decoded))
}
copy(k[:], decoded)
return nil
}
func (k EncryptionKey) MarshalJSON() ([]byte, error) {
return json.Marshal(base64.StdEncoding.EncodeToString(k[:]))
}
func Encrypt(data []byte, key EncryptionKey) ([]byte, error) {
block, err := aes.NewCipher(key.Bytes())
if err != nil {
return nil, err
}
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ciphertext := aesgcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}
func Decrypt(data []byte, key EncryptionKey) ([]byte, error) {
block, err := aes.NewCipher(key.Bytes())
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(data) < 12 {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := data[:12], data[12:]
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("cannot decrypt: %w", err)
}
return plaintext, nil
}