Files

109 lines
2.9 KiB
Go

package crypto
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
corecrypto "git.ilapage.cn/OPC/chorus/internal/core/crypto"
)
const envelopeVersion uint8 = 1
var (
ErrInvalidKeyRing = errors.New("invalid key ring")
ErrUnknownKey = errors.New("envelope key is unavailable")
ErrInvalidEnvelope = errors.New("invalid encrypted envelope")
)
type KeyRing struct {
activeKeyID string
keys map[string][]byte
random io.Reader
}
func NewKeyRing(activeKeyID string, keys map[string][]byte) (*KeyRing, error) {
if activeKeyID == "" || len(keys) == 0 {
return nil, ErrInvalidKeyRing
}
copied := make(map[string][]byte, len(keys))
for keyID, key := range keys {
if keyID == "" {
return nil, ErrInvalidKeyRing
}
if _, err := aes.NewCipher(key); err != nil {
return nil, fmt.Errorf("%w: key %q has an unsupported length", ErrInvalidKeyRing, keyID)
}
copied[keyID] = append([]byte(nil), key...)
}
if _, ok := copied[activeKeyID]; !ok {
return nil, ErrInvalidKeyRing
}
return &KeyRing{activeKeyID: activeKeyID, keys: copied, random: rand.Reader}, nil
}
func (r *KeyRing) Encrypt(ctx context.Context, plaintext []byte) (corecrypto.Envelope, error) {
if err := ctx.Err(); err != nil {
return corecrypto.Envelope{}, err
}
gcm, err := r.gcm(r.activeKeyID)
if err != nil {
return corecrypto.Envelope{}, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(r.random, nonce); err != nil {
return corecrypto.Envelope{}, fmt.Errorf("generate encryption nonce: %w", err)
}
envelope := corecrypto.Envelope{Version: envelopeVersion, KeyID: r.activeKeyID, Nonce: nonce}
envelope.Ciphertext = gcm.Seal(nil, nonce, plaintext, associatedData(envelope.Version, envelope.KeyID))
return envelope, nil
}
func (r *KeyRing) Decrypt(ctx context.Context, envelope corecrypto.Envelope) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if envelope.Version != envelopeVersion || envelope.KeyID == "" {
return nil, ErrInvalidEnvelope
}
gcm, err := r.gcm(envelope.KeyID)
if err != nil {
return nil, err
}
if len(envelope.Nonce) != gcm.NonceSize() || len(envelope.Ciphertext) < gcm.Overhead() {
return nil, ErrInvalidEnvelope
}
plaintext, err := gcm.Open(nil, envelope.Nonce, envelope.Ciphertext, associatedData(envelope.Version, envelope.KeyID))
if err != nil {
return nil, ErrInvalidEnvelope
}
return plaintext, nil
}
func (r *KeyRing) gcm(keyID string) (cipher.AEAD, error) {
key, ok := r.keys[keyID]
if !ok {
return nil, ErrUnknownKey
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, ErrInvalidKeyRing
}
return cipher.NewGCM(block)
}
func associatedData(version uint8, keyID string) []byte {
data := make([]byte, 3+len(keyID))
data[0] = version
binary.BigEndian.PutUint16(data[1:3], uint16(len(keyID)))
copy(data[3:], keyID)
return data
}
var _ corecrypto.KeyCipher = (*KeyRing)(nil)