oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1// Package seal encrypts purpose-bound values for app handoffs and local storage.
2package seal
3
4import (
5 "crypto/aes"
6 "crypto/cipher"
7 "crypto/hkdf"
8 "crypto/rand"
9 "crypto/sha256"
10 "errors"
11)
12
13// Sealer owns a copy of the key and an application namespace.
14type Sealer struct {
15 dek []byte
16 namespace string
17}
18
19// New requires a 32-byte key and an explicit namespace. Keep the namespace
20// stable: changing it makes previously sealed values unreadable.
21func New(dek []byte, namespace string) (*Sealer, error) {
22 if len(dek) != 32 {
23 return nil, errors.New("seal: DEK must be 32 bytes")
24 }
25 if len(namespace) == 0 || len(namespace) > 128 {
26 return nil, errors.New("seal: invalid namespace")
27 }
28 for _, b := range []byte(namespace) {
29 if !(b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '/' || b == '_' || b == '-') {
30 return nil, errors.New("seal: invalid namespace")
31 }
32 }
33 return &Sealer{dek: append([]byte(nil), dek...), namespace: namespace}, nil
34}
35
36func (s *Sealer) purposeKey(purpose string) ([]byte, error) {
37 return hkdf.Key(sha256.New, s.dek, nil, s.namespace+":"+purpose, 32)
38}
39func (s *Sealer) aeadFor(purpose string) (cipher.AEAD, error) {
40 key, err := s.purposeKey(purpose)
41 if err != nil {
42 return nil, err
43 }
44 block, err := aes.NewCipher(key)
45 if err != nil {
46 return nil, err
47 }
48 return cipher.NewGCM(block)
49}
50
51// Seal uses the historical Oficina envelope: random nonce followed by GCM
52// ciphertext and authentication tag. The namespace and purpose bind the key.
53func (s *Sealer) Seal(purpose string, plaintext []byte) ([]byte, error) {
54 aead, err := s.aeadFor(purpose)
55 if err != nil {
56 return nil, err
57 }
58 nonce := make([]byte, aead.NonceSize())
59 if _, err := rand.Read(nonce); err != nil {
60 return nil, err
61 }
62 return aead.Seal(nonce, nonce, plaintext, nil), nil
63}
64
65// Open refuses a different key, namespace or purpose, and altered ciphertext.
66func (s *Sealer) Open(purpose string, ct []byte) ([]byte, error) {
67 aead, err := s.aeadFor(purpose)
68 if err != nil {
69 return nil, err
70 }
71 if len(ct) < aead.NonceSize() {
72 return nil, errors.New("seal: ciphertext too short")
73 }
74 return aead.Open(nil, ct[:aead.NonceSize()], ct[aead.NonceSize():], nil)
75}
76