-
Notifications
You must be signed in to change notification settings - Fork 5
/
stream.go
340 lines (300 loc) · 9.79 KB
/
stream.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.
package stream
import (
"encoding/binary"
"errors"
"fmt"
"io"
"math/bits"
"runtime"
ntrup "github.com/companyzero/sntrup4591761"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
)
// counter implements a 12-byte little endian counter suitable for use as an
// incrementing ChaCha20-Poly1305 nonce.
type counter struct {
limbs [3]uint32
bytes []byte
}
func newCounter() *counter {
return &counter{bytes: make([]byte, 12)}
}
func (c *counter) inc() {
var carry uint32
c.limbs[0], carry = bits.Add32(c.limbs[0], 1, carry)
c.limbs[1], carry = bits.Add32(c.limbs[1], 0, carry)
c.limbs[2], carry = bits.Add32(c.limbs[2], 0, carry)
if carry == 1 {
panic("nonce reuse")
}
binary.LittleEndian.PutUint32(c.bytes[0:4], c.limbs[0])
binary.LittleEndian.PutUint32(c.bytes[4:8], c.limbs[1])
binary.LittleEndian.PutUint32(c.bytes[8:12], c.limbs[2])
}
// PublicKey is a type alias for a properly-sized byte array to represent a
// Streamlined NTRU Prime 4591^761 public key.
type PublicKey = [ntrup.PublicKeySize]byte
// SecretKey is a type alias for a properly-sized byte array to represent a
// Streamlined NTRU Prime 4591^761 secret key.
type SecretKey = [ntrup.PrivateKeySize]byte
// Ciphertext is a type alias for a properly-sized byte array to represent the
// ciphertext of a Streamlined NTRU Prime 4591^761 encapsulated key.
type Ciphertext = [ntrup.CiphertextSize]byte
// SymmetricKey is a type alias for a properly-sized byte array for a
// ChaCha20-Poly1305 symmetric encryption key.
type SymmetricKey = [chacha20poly1305.KeySize]byte
const streamVersion = 3
const chunksize = 1 << 16 // does not include AEAD overhead
const overhead = 16 // poly1305 tag overhead
// KeyScheme describes the keying scheme used for message encryption. It is
// recorded in the stream header, and decrypters must first parse the scheme
// from the header before deriving or recovering the encryption key.
type KeyScheme byte
// Key schemes
const (
StreamlinedNTRUPrime4591761Scheme KeyScheme = iota + 1
Argon2idScheme
)
// Encapsulate creates the header beginning a PKI encryption stream. It derives
// an ephemeral ChaCha20-Poly1305 symmetric key and encapsulates (encrypts) the
// key for the public key pk, recording the key ciphertext in the header.
// Cryptographically-secure randomness is read from rand.
func Encapsulate(rand io.Reader, pk *PublicKey) (header []byte, key *SymmetricKey, err error) {
// Derive and encapsulate an ephemeral shared symmetric key to encrypt a
// message that can only be decapsulated using pk's secret key.
sharedKeyCiphertext, sharedKeyPlaintext, err := ntrup.Encapsulate(rand, pk)
if err != nil {
return
}
header = make([]byte, 1+len(sharedKeyCiphertext))
header[0] = byte(StreamlinedNTRUPrime4591761Scheme)
copy(header[1:], sharedKeyCiphertext[:])
return header, sharedKeyPlaintext, nil
}
// PassphraseHeader creates the header beginning a passphrase-protected encryption stream.
// The time and memory parameters describe Argon2id difficulty parameters, where
// memory is measured in KiB.
// Cryptographically-secure randomness is read from rand.
func PassphraseHeader(rand io.Reader, passphrase []byte, time, memory uint32) (header []byte, key *SymmetricKey, err error) {
threads := uint8(runtime.NumCPU())
header = make([]byte, 1+16+9+overhead)
header[0] = byte(Argon2idScheme)
salt := header[1:17]
htime := header[17 : 17+4]
hmemory := header[17+4 : 17+8]
hthreads := header[17+8 : 17+9]
data := header[:17+9]
htag := header[17+9:]
// Read random salt and store to header
_, err = io.ReadFull(rand, salt)
if err != nil {
return
}
// Write time, memory, and threads
binary.LittleEndian.PutUint32(htime, time)
binary.LittleEndian.PutUint32(hmemory, memory)
hthreads[0] = threads
// Derive a 64-byte Argon2id key from the passphrase.
// The first 32 bytes becomes an authentication key, allowing detection of an
// invalid passphrase during derivation from the header values, rather than
// hitting authentication errors during stream decryption.
// The final 32 bytes is the stream symmetric key.
idkey := argon2.IDKey(passphrase, salt, time, memory, threads, 64)
// Authenticate key derivation
var tag [overhead]byte
var polyKey [32]byte
copy(polyKey[:], idkey[:32])
poly1305.Sum(&tag, data, &polyKey)
copy(htag, tag[:])
key = new(SymmetricKey)
copy(key[:], idkey[32:])
return header, key, nil
}
// Encrypt performs symmetric stream encryption, reading plaintext from r and
// writing an encrypted stream to w which can only be decrypted with knowledge
// of key. The steam header is Associated Data.
func Encrypt(w io.Writer, r io.Reader, header []byte, key *SymmetricKey) error {
buf := make([]byte, 0, chunksize+overhead)
aead, err := chacha20poly1305.New(key[:])
if err != nil {
return err
}
// # Protocol
//
// Keying Header
// - Uniquely describes keying scheme and carries related data.
// Examples include sntrup4591651 encapsulation or KDF parameters.
//
// Version
// - ChaCha20-Poly1305 sealed protocol version, using a zero nonce, with
// header as the Associated Data.
// Version is currently 3, and no other versions are implemented.
// Future versions may allow description of the chunk size or
//
// Blocks
// - ChaCha20-Poly1305 chunked payloads (incrementing previous nonce)
// Write header
_, err = w.Write(header)
if err != nil {
return err
}
// Write sealed version
buf = buf[:4]
binary.LittleEndian.PutUint32(buf, streamVersion)
nonce := newCounter()
buf = aead.Seal(buf[:0], nonce.bytes, buf, header)
_, err = w.Write(buf)
if err != nil {
return err
}
// Read/write chunks
for {
chunk := buf[:chunksize]
l, err := io.ReadFull(r, chunk)
if l > 0 {
nonce.inc()
chunk = aead.Seal(chunk[:0], nonce.bytes, chunk[:l], nil)
_, err := w.Write(chunk)
if err != nil {
return err
}
}
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil
}
if err != nil {
return err
}
}
}
// Header represents a parsed stream header. It records the keying scheme for
// the stream symmetric key, as well as parameters needed to derive the key
// given the specific scheme. The Bytes field records the raw bytes of the full
// header, which must be passed to Decrypt for authentication.
type Header struct {
Bytes []byte
Scheme KeyScheme
// For StreamlinedNTRUPrime4591761Scheme
Ciphertext *Ciphertext
// For Argon2idScheme
Salt []byte
Time uint32
Memory uint32
Threads uint8
Tag [16]byte
}
// ReadHeader parses the stream header from the reader.
func ReadHeader(r io.Reader) (*Header, error) {
var scheme [1]byte
_, err := io.ReadFull(r, scheme[:])
if err != nil {
return nil, err
}
h := new(Header)
h.Scheme = KeyScheme(scheme[0])
switch h.Scheme {
default:
return nil, fmt.Errorf("stream: unknown key scheme %#0x", h.Scheme)
case StreamlinedNTRUPrime4591761Scheme:
h.Ciphertext = new(Ciphertext)
h.Bytes = make([]byte, 1+len(h.Ciphertext))
h.Bytes[0] = scheme[0]
_, err = io.ReadFull(r, h.Bytes[1:])
if err != nil {
return nil, err
}
copy(h.Ciphertext[:], h.Bytes[1:])
case Argon2idScheme:
h.Bytes = make([]byte, 1+16+9+overhead)
h.Bytes[0] = scheme[0]
_, err = io.ReadFull(r, h.Bytes[1:])
if err != nil {
return nil, err
}
h.Salt = h.Bytes[1 : 1+16]
h.Time = binary.LittleEndian.Uint32(h.Bytes[17:])
h.Memory = binary.LittleEndian.Uint32(h.Bytes[17+4 : 17+8])
h.Threads = h.Bytes[17+8]
copy(h.Tag[:], h.Bytes[17+9:])
}
return h, nil
}
// Decapsulate decrypts a PKI encrypted symmetric key from the header.
// The scheme must be for PKI encryption.
func Decapsulate(h *Header, sk *SecretKey) (*SymmetricKey, error) {
if h.Scheme != StreamlinedNTRUPrime4591761Scheme {
return nil, errors.New("stream: nothing to decapsulate in header")
}
sharedKeyPlaintext, ok := ntrup.Decapsulate(h.Ciphertext, sk)
if ok != 1 {
return nil, errors.New("stream: cannot decapsulate message key")
}
return sharedKeyPlaintext, nil
}
// PassphraseKey derives a symmetric key from a passphrase.
// The header scheme must be for symmetric passphrase encryption.
func PassphraseKey(h *Header, passphrase []byte) (*SymmetricKey, error) {
if h.Scheme != Argon2idScheme {
return nil, errors.New("stream: not a symmetric passphrase encryption scheme")
}
idkey := argon2.IDKey(passphrase, h.Salt, h.Time, h.Memory, h.Threads, 64)
// Authenticate key derivation
var polyKey [32]byte
copy(polyKey[:], idkey[:32])
data := h.Bytes[:17+9]
if !poly1305.Verify(&h.Tag, data, &polyKey) {
return nil, errors.New("stream: incorrect passphrase")
}
key := new(SymmetricKey)
copy(key[:], idkey[32:])
return key, nil
}
// Decrypt performs symmetric stream decryption, reading ciphertext from r,
// decrypting with key, and writing a stream of plaintext to w. The steam
// header is Associated Data.
func Decrypt(w io.Writer, r io.Reader, header []byte, key *SymmetricKey) error {
nonce := newCounter()
buf := make([]byte, 0, chunksize+overhead)
aead, err := chacha20poly1305.New(key[:])
if err != nil {
return err
}
// Read sealed version
buf = buf[:4+overhead]
_, err = io.ReadFull(r, buf)
if err != nil {
return err
}
buf, err = aead.Open(buf[:0], nonce.bytes, buf, header)
if err != nil {
return err
}
if binary.LittleEndian.Uint32(buf) != streamVersion {
return fmt.Errorf("unknown protocol version %x", buf)
}
for {
chunk := buf[:chunksize+overhead]
l, err := io.ReadFull(r, chunk)
if l > 0 {
var err error
nonce.inc()
chunk, err = aead.Open(chunk[:0], nonce.bytes, chunk[:l], nil)
if err != nil {
return err
}
_, err = w.Write(chunk)
if err != nil {
return err
}
}
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil
}
if err != nil {
return err
}
}
}