oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package seal is the app's only encryption surface.
2//
3// The one rule (spec §0.1): a meeting's title is sealed at rest, and
4// the database never holds a plaintext title or an outsider's address.
5// The DEK arrives as platform config and is never written to the
6// database it decrypts.
7//
8// Lifted from calendar's seal package, itself lifted from sheets', with
9// only the label changed. Calendar names the third app as the trigger
10// to extract this into estilo or rastrillo; Calls is that app, and the
11// extraction is a task on the plan, not a change here.
12package seal
13
14import (
15 "crypto/aes"
16 "crypto/cipher"
17 "crypto/rand"
18 "crypto/sha256"
19 "errors"
20 "hash"
21 "io"
22
23 "golang.org/x/crypto/hkdf"
24)
25
26// label is mixed into every derived key. Changing it makes every
27// existing sealed value unreadable, so it is a constant, not config.
28const label = "oficina/calls:"
29
30const (
31 PurposeMeetingTitle = "meeting.title"
32 PurposeMeetingLink = "meeting.link"
33 PurposePendingAddress = "pending.address"
34)
35
36func sha256New() hash.Hash { return sha256.New() }
37
38type Sealer struct{ dek []byte }
39
40// New refuses anything but a 32-byte key and keeps its own copy.
41func New(dek []byte) (*Sealer, error) {
42 if len(dek) != 32 {
43 return nil, errors.New("seal: the DEK must be exactly 32 bytes")
44 }
45 k := make([]byte, 32)
46 copy(k, dek)
47 return &Sealer{dek: k}, nil
48}
49
50func (s *Sealer) purposeKey(purpose string) ([]byte, error) {
51 key := make([]byte, 32)
52 r := hkdf.New(sha256New, s.dek, nil, []byte(label+purpose))
53 if _, err := io.ReadFull(r, key); err != nil {
54 return nil, err
55 }
56 return key, nil
57}
58
59func (s *Sealer) aeadFor(purpose string) (cipher.AEAD, error) {
60 key, err := s.purposeKey(purpose)
61 if err != nil {
62 return nil, err
63 }
64 block, err := aes.NewCipher(key)
65 if err != nil {
66 return nil, err
67 }
68 return cipher.NewGCM(block)
69}
70
71// Seal returns nonce || ciphertext.
72func (s *Sealer) Seal(purpose string, plaintext []byte) ([]byte, error) {
73 aead, err := s.aeadFor(purpose)
74 if err != nil {
75 return nil, err
76 }
77 nonce := make([]byte, aead.NonceSize())
78 if _, err := rand.Read(nonce); err != nil {
79 return nil, err
80 }
81 return aead.Seal(nonce, nonce, plaintext, nil), nil
82}
83
84func (s *Sealer) Open(purpose string, ct []byte) ([]byte, error) {
85 aead, err := s.aeadFor(purpose)
86 if err != nil {
87 return nil, err
88 }
89 if len(ct) < aead.NonceSize() {
90 return nil, errors.New("seal: ciphertext too short")
91 }
92 nonce, body := ct[:aead.NonceSize()], ct[aead.NonceSize():]
93 return aead.Open(nil, nonce, body, nil)
94}
95
96// SealString seals a string; an empty string seals to nil so an
97// optional column stays NULL.
98func (s *Sealer) SealString(purpose, plaintext string) ([]byte, error) {
99 if plaintext == "" {
100 return nil, nil
101 }
102 return s.Seal(purpose, []byte(plaintext))
103}
104
105func (s *Sealer) OpenString(purpose string, ct []byte) (string, error) {
106 if len(ct) == 0 {
107 return "", nil
108 }
109 b, err := s.Open(purpose, ct)
110 if err != nil {
111 return "", err
112 }
113 return string(b), nil
114}
115