Initial project snapshot
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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)),
|
||||
}, "|"))
|
||||
}
|
||||
Reference in New Issue
Block a user