carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package dbreads
2
3import (
4 "crypto/aes"
5 "crypto/cipher"
6 "crypto/ecdh"
7 "crypto/rand"
8 "crypto/sha256"
9 "encoding/base64"
10 "encoding/binary"
11 "encoding/json"
12 "errors"
13 "io"
14 "strconv"
15
16 "golang.org/x/crypto/hkdf"
17)
18
19// Sealing is the appconfig v2 construction (spec §5.6): ECDH P-256 with a
20// fresh sender key per seal, HKDF-SHA256 with this package's own info string,
21// AES-256-GCM with bound associated data. The wire is raw bytes, never
22// base64, in the bucket: "DBR1" || sender key (65) || nonce (12) || ciphertext.
23const (
24 sealPrefix = "DBR1"
25 sealEphLen = 65
26 sealNonceLen = 12
27 // SealOverhead is what sealing adds: under 100 bytes (spec §5.7).
28 SealOverhead = len(sealPrefix) + sealEphLen + sealNonceLen + 16
29)
30
31// The streams a chunk can carry, bound into its associated data.
32const (
33 StreamRows = "rows" // a query's output
34 StreamLTX = "ltx" // a pull's planned files (plan 3)
35)
36
37// ErrSeal is any sealed object that does not open: wrong key, moved,
38// reordered, relabelled, truncated or corrupt.
39var ErrSeal = errors.New("dbreads: the sealed bytes do not open")
40
41var errBadReplyKey = errors.New("dbreads: reply_key is not an uncompressed P-256 point")
42
43// randReader is a var so a test can make sealing deterministic.
44var randReader io.Reader = rand.Reader
45
46// NewReplyKey is the CLI's ephemeral key pair for one read. The private half
47// never leaves the CLI's memory.
48func NewReplyKey() (*ecdh.PrivateKey, error) { return ecdh.P256().GenerateKey(randReader) }
49
50func EncodeReplyKey(pub *ecdh.PublicKey) string {
51 return base64.StdEncoding.EncodeToString(pub.Bytes())
52}
53
54func ParseReplyKey(s string) (*ecdh.PublicKey, error) {
55 raw, err := base64.StdEncoding.Strict().DecodeString(s)
56 if err != nil || len(raw) != sealEphLen {
57 return nil, errBadReplyKey
58 }
59 pub, err := ecdh.P256().NewPublicKey(raw)
60 if err != nil {
61 return nil, errBadReplyKey
62 }
63 return pub, nil
64}
65
66// lengthPrefixed encodes fields so no choice of values makes two lists
67// encode the same.
68func lengthPrefixed(fields ...string) []byte {
69 var out []byte
70 for _, f := range fields {
71 out = binary.BigEndian.AppendUint32(out, uint32(len(f)))
72 out = append(out, f...)
73 }
74 return out
75}
76
77func chunkAAD(id string, seq int64, stream string, last bool) []byte {
78 l := "0"
79 if last {
80 l = "1"
81 }
82 return lengthPrefixed(Purpose, "chunk", id, strconv.FormatInt(seq, 10), stream, l)
83}
84
85func summaryAAD(id string) []byte { return lengthPrefixed(Purpose, "summary", id) }
86
87func gcmFor(shared []byte) (cipher.AEAD, error) {
88 key := make([]byte, 32)
89 if _, err := io.ReadFull(hkdf.New(sha256.New, shared, nil, []byte(Purpose)), key); err != nil {
90 return nil, err
91 }
92 block, err := aes.NewCipher(key)
93 if err != nil {
94 return nil, err
95 }
96 return cipher.NewGCM(block)
97}
98
99func seal(pub *ecdh.PublicKey, aad, plain []byte) ([]byte, error) {
100 eph, err := ecdh.P256().GenerateKey(randReader)
101 if err != nil {
102 return nil, err
103 }
104 nonce := make([]byte, sealNonceLen)
105 if _, err := io.ReadFull(randReader, nonce); err != nil {
106 return nil, err
107 }
108 shared, err := eph.ECDH(pub)
109 if err != nil {
110 return nil, err
111 }
112 gcm, err := gcmFor(shared)
113 if err != nil {
114 return nil, err
115 }
116 out := make([]byte, 0, SealOverhead+len(plain))
117 out = append(out, sealPrefix...)
118 out = append(out, eph.PublicKey().Bytes()...)
119 out = append(out, nonce...)
120 return gcm.Seal(out, nonce, plain, aad), nil
121}
122
123func open(priv *ecdh.PrivateKey, aad, sealed []byte) ([]byte, error) {
124 if len(sealed) < SealOverhead || string(sealed[:len(sealPrefix)]) != sealPrefix {
125 return nil, ErrSeal
126 }
127 body := sealed[len(sealPrefix):]
128 ephPub, err := ecdh.P256().NewPublicKey(body[:sealEphLen])
129 if err != nil {
130 return nil, ErrSeal
131 }
132 shared, err := priv.ECDH(ephPub)
133 if err != nil {
134 return nil, ErrSeal
135 }
136 gcm, err := gcmFor(shared)
137 if err != nil {
138 return nil, ErrSeal
139 }
140 plain, err := gcm.Open(nil, body[sealEphLen:sealEphLen+sealNonceLen], body[sealEphLen+sealNonceLen:], aad)
141 if err != nil {
142 return nil, ErrSeal
143 }
144 return plain, nil
145}
146
147// SealChunk seals one chunk of a read's output: bound to the read, its
148// sequence number, its stream and whether it is the last.
149func SealChunk(pub *ecdh.PublicKey, id string, seq int64, stream string, last bool, plain []byte) ([]byte, error) {
150 return seal(pub, chunkAAD(id, seq, stream, last), plain)
151}
152
153func OpenChunk(priv *ecdh.PrivateKey, id string, seq int64, stream string, last bool, sealed []byte) ([]byte, error) {
154 return open(priv, chunkAAD(id, seq, stream, last), sealed)
155}
156
157// SealSummary seals the result's summary (base64, it rides in result.json).
158func SealSummary(pub *ecdh.PublicKey, id string, s Summary) (string, error) {
159 raw, err := json.Marshal(s)
160 if err != nil {
161 return "", err
162 }
163 sealed, err := seal(pub, summaryAAD(id), raw)
164 if err != nil {
165 return "", err
166 }
167 return base64.StdEncoding.EncodeToString(sealed), nil
168}
169
170func OpenSummary(priv *ecdh.PrivateKey, id, sealed string) (Summary, error) {
171 raw, err := base64.StdEncoding.Strict().DecodeString(sealed)
172 if err != nil {
173 return Summary{}, ErrSeal
174 }
175 plain, err := open(priv, summaryAAD(id), raw)
176 if err != nil {
177 return Summary{}, err
178 }
179 var s Summary
180 if err := json.Unmarshal(plain, &s); err != nil {
181 return Summary{}, ErrSeal
182 }
183 return s, nil
184}
185