109 lines
2.9 KiB
Go
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)
|