-
Notifications
You must be signed in to change notification settings - Fork 1
/
message.coffee
366 lines (291 loc) · 14.9 KB
/
message.coffee
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
crypto = require 'crypto'
ursa = require 'ursa'
msgpack = require 'msgpack'
stream = require 'stream'
keyLib = require './key'
Buffer.prototype.toArray = -> Array.prototype.slice.call this, 0
# Standard and recommended class for encrypting and authenticating stream-like
# data.
#
# 1. `keys` is a hash of key-related data. May contain the following fields:
# - `key` - A random symmetric key, usually through
# `caesar.key.createRandom()`. If present, this key will be used on all
# messages. If not present, a random key will be created for each message.
# - `public` - An array or hash of public keys. If a hash, the key's owner
# can be identified easier (by anybody--not just the owner). If present, a
# keyring will be available to the owners of the corresponding private keys
# that allows them to read the message without any previous knowledge of
# the symmetric key.
# - `private` - An array or hash of private keys. If a hash, the key's owner
# can be identified easier (by anybody--not just the owner). If present,
# asymmetric authentication can be used (signatures), which is publicly
# verifiable.
# 2. `confidential` is whether or not the data should be encrypted to prevent
# eavesdropping. *(Boolean)*
# 3. `integrous` is the type of integrity that should be maintained. Can be
# null (no integrity), "sym" (symmetric integrity, verifiable by others with
# the secret key), and "asym" (asymmetric integrity, verifiable by anyone).
# *(String)*
# 4. `cut` is the maximum size of a plaintext chunk (where it should be cut).
# If you get strange errors, try lowering this below the default. *(Number)*
class exports.Encrypter extends stream.Transform
constructor: (@keys, @confidential, @integrous, @cut = 14336) ->
if not this instanceof exports.Encrypter
return new exports.Encrypter @keys, @confidential, @integrous, @cut
stream.Transform.call this, objectMode: true, decodeStrings: true
@leftover = new Buffer 0
if @confidential isnt true and @confidential isnt false
throw 'Confidential must be true or false.'
if @integrous? and @integrous isnt 'sym' and @integrous isnt 'asym'
throw 'Integrous must be null, sym, or asym.'
requiresKey = @confidential or @integrous is 'sym'
if requiresKey and not @keys.key? and not @keys.public?
throw 'No symmetric or public key.'
if @integrous is 'asym' and not @keys.private?
throw 'No private key to sign messages with.'
if not @confidential then @buffer = new stream.PassThrough()
else if @keys.key?
@buffer = crypto.createCipher 'aes-256-ctr', @keys.key
_transform: (dump, encoding, done) ->
while dump.length isnt 0 # Chop up dump.
len = if dump.length > @cut then @cut else dump.length
chunk = new Buffer len
dump.copy chunk
dump = dump.slice len
if not @keys.key? # Generate random key if needed.
key = keyLib.createRandom()
@buffer = crypto.createCipher 'aes-256-ctr', key
else key = @keys.key
@buffer.write chunk
data = @buffer.read()
footer = {}
if @integrous is 'sym' # Calculate HMAC.
mac = crypto.createHmac 'sha512', key
mac.end data
footer.mac = mac.read().toArray()
if @integrous is 'asym' # Calculate signature.
sigs = {}
footer.sigs = if @keys.private instanceof Array then [] else {}
sigs[k] = ursa.createSigner 'sha512' for k of @keys.private
sigs[k].update data for k of @keys.private
footer.sigs[k] = sigs[k].sign v for k, v of @keys.private
footer.sigs[k] = v.toArray() for k, v of footer.sigs
if @keys.public? # Calculate keyring.
footer.keys = if @keys.public instanceof Array then [] else {}
footer.keys[k] = v.encrypt key for k, v of @keys.public
footer.keys[k] = v.toArray() for k, v of footer.keys
footer = msgpack.pack footer
out = new Buffer 4 + data.length + footer.length
out.writeUInt16BE data.length, 0
data.copy out, 2, 0, data.length
out.writeUInt16BE footer.length, data.length + 2
footer.copy out, data.length + 4, 0, footer.length
more = @push out
if not more then return done()
done()
_flush: (done) ->
@push if @leftover.length is 0 then null else @leftover
@leftover = new Buffer 0
done()
# Standard and recommended class for decrypting and verifying stream-like data.
#
# 1. `keys` is a hash of key-related data. May contain the following fields:
# - `key` - A random symmetric key, usually through
# `caesar.key.createRandom()`. If present, this key will be used on all
# messages. If not present, a keyring must be present to decrypt messages.
# - `public` - An array or hash of public keys. If a hash, the key's owner
# can be identified easier. If present, they will be used to verify
# signatures.
# - `private` - An array or hash of private keys. If a hash, the key's owner
# can be identified easier. If present, they will be used to derive
# symmetric keys from a keyring.
# 2. `confidential` is whether or not the data should be encrypted to prevent
# eavesdropping. Must match the Encrypter's value. *(Boolean)*
# 3. `integrous` is the type of integrity that should be maintained. Can be
# null (no integrity), "sym" (symmetric integrity, verifiable by others with
# the secret key), and "asym" (asymmetric integrity, verifiable by anyone).
# Must match the Encrypter's value. *(String)*
class exports.Decrypter extends stream.Transform
constructor: (@keys, @confidential, @integrous) ->
if not this instanceof exports.Decrypter
return new exports.Decrypter @keys, @confidential, @integrous
stream.Transform.call this, decodeStrings: true
if @confidential isnt true and @confidential isnt false
throw 'Confidential must be true or false.'
if @integrous? and @integrous isnt 'sym' and @integrous isnt 'asym'
throw 'Integrous must be null, sym, or asym.'
requiresKey = @confidential or @integrous is 'sym'
if requiresKey and not @keys.key? and not @keys.private?
throw 'No symmetric or private key.'
if @keys.key? and @confidential
@cipher = crypto.createDecipher 'aes-256-ctr', @keys.key
else @cipher = new stream.PassThrough()
_transform: (chunk, encoding, done) ->
dlen = chunk.readUInt16BE 0
data = chunk.slice 2, 2 + dlen
flen = chunk.readUInt16BE 2 + dlen
footer = msgpack.unpack chunk.slice 4 + dlen
if not data? then return done 'No payload.'
if not footer.mac? and @integrous is 'sym' then return done 'No MAC.'
if not footer.sigs? and @integrous is 'asym' then return done 'No sigs.'
requiresKey = @confidential or @integrous is 'sym'
if requiresKey and not @keys.key? and not footer.keys?
return done 'No method of key derivation.'
key = null
if @keys.key? # Derive key if needed.
key = @keys.key
else
if footer.keys instanceof Array
for tag in footer.keys
for n, privKey of @keys.private
try key = privKey.decrypt tag
catch err then key = null
if key? then break
if key? then break
else
keys = (key for key of @keys.private when footer.keys[key]?)
if keys.length is 0
return done 'No valid keys for decryption.'
for k in keys
try key = @keys.private[k].decrypt new Buffer footer.keys[k]
catch err then key = null
if key? then break
if not key? then return done 'No key was successfully derived.'
if not @keys.key? # Intialize new cipher if needed.
@cipher = crypto.createDecipher 'aes-256-ctr', key
if @integrous is 'sym' # Verify an HMAC.
mac = crypto.createHmac 'sha512', key
mac.end new Buffer footer.mac
candidateTag = mac.read().toString 'base64'
mac = crypto.createHmac 'sha512', key
mac.end data
tag = mac.read()
mac = crypto.createHmac 'sha512', key
mac.end tag
tag = mac.read().toString 'base64'
if candidateTag isnt tag then return done 'Bad MAC.'
if @integrous is 'asym' # Verify a signature.
ok = false
if footer.sigs instanceof Array
for sig in footer.sigs
sig = new Buffer sig
for pubKey in @keys.public
v = ursa.createVerifier 'sha512'
v.update data
ok = v.verify pubKey, sig, 'base64'
if ok then break
if ok then break
else
keys = (key for key of @keys.public when footer.sigs[key]?)
if keys.length is 0 then return done 'No valid keys for auth.'
for k in keys
try
v = ursa.createVerifier 'sha512'
v.update data
ok = v.verify @keys.public[k], new Buffer footer.sigs[k]
if ok then break
catch e
if not ok then return done 'Bad signature.'
@cipher.write data # Decrypt the message.
@push @cipher.read()
done()
# An implementation of Synthetic IV encryption. SIV is a method of
# deterministic and authenticated encryption, commonly used in encrypted
# databases and insecure keystores. This is because given data can be encrypted
# and used in a search for other related SIV encrypted information which can
# then be decrypted (unlike with a hash, for example). Because SIV is
# deterministic, only use it on data whose structure prevents repetition (like
# user ids, usernames, or randomly generated keys).
#
# 1. `key1` - Randomly generated symmetric key, usually through
# `caesar.key.createRandom()`.
# 2. `key2` - See above. Should be different from key1.
class exports.SIVEncrypter extends stream.Transform
constructor: (@key1, @key2) ->
if not this instanceof exports.SIVEncrypter
return new exports.SIVEncrypter @key1, @key2
stream.Transform.call this, objectMode: true, decodeStrings: true
@leftover = new Buffer 0
_transform: (dump, encoding, done) ->
while dump.length isnt 0 # Chop up dump.
len = if dump.length > 16368 then 16368 else dump.length
chunk = new Buffer len
dump.copy chunk
dump = dump.slice len
hash = crypto.createHash 'sha256' # Calculate IV.
hash.end chunk
tag = hash.read()
temp = crypto.createCipher 'aes-256-ctr', @key1
temp.end tag
iv = temp.read().slice 0, 16
cipher = crypto.createCipheriv 'aes-256-ctr', @key2, iv # Encrypt.
cipher.end chunk
end = Buffer.concat [iv, cipher.read()]
more = @push end
if not more then return done()
done()
_flush: (done) ->
@push if @leftover.length is 0 then null else @leftover
@leftover = new Buffer 0
done()
# An implementation of Synthetic IV decryption. See above.
class exports.SIVDecrypter extends stream.Transform
constructor: (@key1, @key2) ->
if not this instanceof exports.SIVDecrypter
return new exports.SIVDecrypter @key1, @key2
stream.Transform.call this, decodeStrings: true
_transform: (chunk, encoding, done) ->
iv = chunk.slice 0, 16
data = chunk.slice 16
decipher = crypto.createDecipheriv 'aes-256-ctr', @key2, iv # Decrypt
decipher.end data
pt = decipher.read()
hash = crypto.createHash 'sha256' # Authenticate by calculating IV
hash.end pt
tag = hash.read()
temp = crypto.createCipher 'aes-256-ctr', @key1
temp.end tag
ivCand = temp.read().slice 0, 16
if iv.toString('base64') isnt ivCand.toString('base64')
return done 'Failed auth.'
@push pt
done()
# An implementation of XTS encryption. XTS is size-preserving encryption used
# for randomly accessible data, like RAM or hard disks, which are split
# into sectors of fixed size. Because size must be preserved, no authentication
# can be used, meaning any ciphertext can be altered by an attacker and will
# still decrypt.
#
# 1. `key` - Randomly generated symmetric key, usually through
# `caesar.key.createRandom()`.
# 2. `cut` - The size in bytes of each sector. If the size of the plaintext is
# not divisible by the cut then 0x00s are appended.
class exports.XTSEncrypter extends stream.Transform
constructor: (@key, @cut = 32) ->
if not this instanceof exports.XTSEncrypter
return new exports.XTSEncrypter @key, @cut
stream.Transform.call this, decodeStrings: true
@cipher = crypto.createCipher 'aes-256-xts', @key
_transform: (dump, encoding, done) ->
while dump.length isnt 0 # Chop up dump.
chunk = new Buffer @cut
dump.copy chunk
dump = dump.slice @cut
@cipher.write chunk
@push @cipher.read()
done()
# An implementation of XTS decryption. See above.
class exports.XTSDecrypter extends stream.Transform
constructor: (@key, @cut = 32) ->
if not this instanceof exports.XTSDecrypter
return new exports.XTSDecrypter @key
stream.Transform.call this, decodeStrings: true
@decipher = crypto.createDecipher 'aes-256-xts', @key
_transform: (dump, encoding, done) ->
while dump.length isnt 0 # Chop up dump.
chunk = new Buffer @cut
dump.copy chunk
dump = dump.slice @cut
@decipher.write chunk
@push @decipher.read()
done()