114 lines
2.9 KiB
Go
114 lines
2.9 KiB
Go
package secrets
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
const AlgorithmAES256GCM = "AES-256-GCM"
|
|
|
|
var (
|
|
ErrSecretEncryptionKeyMissing = errors.New("secret encryption key is not configured")
|
|
ErrSecretPayloadInvalid = errors.New("secret payload must be a json object")
|
|
)
|
|
|
|
type Encryptor struct {
|
|
aead cipher.AEAD
|
|
keyID string
|
|
}
|
|
|
|
type EncryptedPayload struct {
|
|
Algorithm string
|
|
KeyID string
|
|
Nonce []byte
|
|
Ciphertext []byte
|
|
PayloadSHA256 string
|
|
}
|
|
|
|
func NewEncryptor(masterKeyBase64, keyID string) (*Encryptor, error) {
|
|
masterKeyBase64 = strings.TrimSpace(masterKeyBase64)
|
|
if masterKeyBase64 == "" {
|
|
return nil, ErrSecretEncryptionKeyMissing
|
|
}
|
|
key, err := base64.StdEncoding.DecodeString(masterKeyBase64)
|
|
if err != nil {
|
|
if rawKey, rawErr := base64.RawStdEncoding.DecodeString(masterKeyBase64); rawErr == nil {
|
|
key = rawKey
|
|
} else {
|
|
return nil, fmt.Errorf("decode secret encryption key: %w", err)
|
|
}
|
|
}
|
|
if len(key) != 32 {
|
|
return nil, fmt.Errorf("secret encryption key must decode to 32 bytes")
|
|
}
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create secret cipher: %w", err)
|
|
}
|
|
aead, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create secret gcm: %w", err)
|
|
}
|
|
if strings.TrimSpace(keyID) == "" {
|
|
keyID = "local-v1"
|
|
}
|
|
return &Encryptor{aead: aead, keyID: keyID}, nil
|
|
}
|
|
|
|
func (e *Encryptor) KeyID() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
return e.keyID
|
|
}
|
|
|
|
func (e *Encryptor) Encrypt(plaintext, aad []byte) (EncryptedPayload, error) {
|
|
if e == nil {
|
|
return EncryptedPayload{}, ErrSecretEncryptionKeyMissing
|
|
}
|
|
nonce := make([]byte, e.aead.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return EncryptedPayload{}, fmt.Errorf("generate secret nonce: %w", err)
|
|
}
|
|
hash := sha256.Sum256(plaintext)
|
|
return EncryptedPayload{
|
|
Algorithm: AlgorithmAES256GCM,
|
|
KeyID: e.keyID,
|
|
Nonce: nonce,
|
|
Ciphertext: e.aead.Seal(nil, nonce, plaintext, aad),
|
|
PayloadSHA256: hex.EncodeToString(hash[:]),
|
|
}, nil
|
|
}
|
|
|
|
func (e *Encryptor) Decrypt(payload EncryptedPayload, aad []byte) ([]byte, error) {
|
|
if e == nil {
|
|
return nil, ErrSecretEncryptionKeyMissing
|
|
}
|
|
if payload.Algorithm != "" && payload.Algorithm != AlgorithmAES256GCM {
|
|
return nil, fmt.Errorf("unsupported secret algorithm %q", payload.Algorithm)
|
|
}
|
|
plaintext, err := e.aead.Open(nil, payload.Nonce, payload.Ciphertext, aad)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decrypt secret payload: %w", err)
|
|
}
|
|
return plaintext, nil
|
|
}
|
|
|
|
func ResourceSecretAAD(organizationID, resourceID, secretRef, protocol string) []byte {
|
|
return []byte(strings.Join([]string{
|
|
"rap-resource-secret-v1",
|
|
strings.TrimSpace(organizationID),
|
|
strings.TrimSpace(resourceID),
|
|
strings.TrimSpace(secretRef),
|
|
strings.ToLower(strings.TrimSpace(protocol)),
|
|
}, "|"))
|
|
}
|