1package oidcrp
2
3import (
4 "crypto/aes"
5 "crypto/cipher"
6 "crypto/hkdf"
7 "crypto/rand"
8 "crypto/sha256"
9 "encoding/base64"
10 "errors"
11 "fmt"
12 "strings"
13)
14
15const (
16 TxnInfo = "tug txn v1"
17 SessionInfo = "tug session v1"
18)
19
20var ErrOpen = errors.New("oidcrp: sealed value rejected")
21
22type Sealer struct {
23 aead cipher.AEAD
24}
25
26func NewSealer(master []byte, info string) (*Sealer, error) {
27 if len(master) != 32 {
28 return nil, errors.New("oidcrp: the master key is not 32 bytes")
29 }
30 key, err := hkdf.Key(sha256.New, master, nil, info, 32)
31 if err != nil {
32 return nil, fmt.Errorf("oidcrp: derive %q: %w", info, err)
33 }
34 block, err := aes.NewCipher(key)
35 if err != nil {
36 return nil, err
37 }
38 aead, err := cipher.NewGCM(block)
39 if err != nil {
40 return nil, err
41 }
42 return &Sealer{aead: aead}, nil
43}
44
45func (s *Sealer) Seal(name string, plaintext []byte) string {
46 nonce := make([]byte, s.aead.NonceSize())
47 rand.Read(nonce)
48 return base64.RawURLEncoding.EncodeToString(s.aead.Seal(nonce, nonce, plaintext, []byte(name)))
49}
50
51func outsideAlphabet(r rune) bool {
52 return !(r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' || r == '_')
53}
54
55func (s *Sealer) Open(name, value string) ([]byte, error) {
56 if strings.IndexFunc(value, outsideAlphabet) >= 0 {
57 return nil, ErrOpen
58 }
59 b, err := base64.RawURLEncoding.Strict().DecodeString(value)
60 n := s.aead.NonceSize()
61 if err != nil || len(b) < n+s.aead.Overhead() {
62 return nil, ErrOpen
63 }
64 plaintext, err := s.aead.Open(nil, b[:n], b[n:], []byte(name))
65 if err != nil {
66 return nil, ErrOpen
67 }
68 return plaintext, nil
69}