-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathsecureMessages.ts
More file actions
365 lines (336 loc) · 11.2 KB
/
secureMessages.ts
File metadata and controls
365 lines (336 loc) · 11.2 KB
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
/**
* All messages sent to the Lattice from this SDK will be
* "secure messages", of which there are two types:
*
* 1. Connect requests are *unencrypted* and serve to establish
* a connection between the SDK Client instance and the target
* Lattice. If the client is already paired to the target Lattice,
* the response will indicate that. If the client has never paired
* with this Lattice, the Lattice will go into "pairing mode" and
* will expect a follow up `finalizePairing` request, which is
* an encrypted request. This will return an ephemeral public key,
* which is used to encrypt the next request.
* 2. Encrypted requests are *encrypted* (obviously) and from a Lattice
* protocol perspective they are all constructed the same way:
* create a buffer of `payload` length and fill it with unencrypted
* data, then encrypt the entire payload (not just the data you filled)
* with the ECDH secret formed from the last ephemeral public key.
* The response to this request will contain a new ephemral public
* key, which you will need for the next encrypted request.
*/
import {
ProtocolConstants as Constants,
LatticeMsgType,
LatticeProtocolVersion,
LatticeSecureEncryptedRequestType,
LatticeSecureMsgType,
} from './latticeConstants';
import {
aes256_decrypt,
aes256_encrypt,
checksum,
getP256KeyPairFromPub,
randomBytes,
} from '../util';
import { getEphemeralId, request } from '../shared/functions';
import { validateEphemeralPub } from '../shared/validators';
import {
DecryptedResponse,
LatticeSecureRequestPayload,
LatticeMessageHeader,
LatticeSecureRequest,
LatticeSecureConnectRequestPayloadData,
LatticeSecureDecryptedResponse,
KeyPair,
} from '../types';
const { msgSizes } = Constants;
const { secure: szs } = msgSizes;
/**
* Build and make a request to connect to a specific Lattice
* based on its `deviceId`.
* @param deviceId - Device ID for the target Lattice. Must be in
* the same `client.baseUrl` domain to be found.
* @return {Buffer} - Connection response payload data, which contains
* information about the connected Lattice.
*/
export async function connectSecureRequest({
url,
pubkey,
}: {
url: string;
pubkey: Buffer;
}): Promise<Buffer> {
// Build the secure request message
const payloadData = serializeSecureRequestConnectPayloadData({
pubkey: pubkey,
});
const msgId = randomBytes(4);
const msg = serializeSecureRequestMsg(
msgId,
LatticeSecureMsgType.connect,
payloadData,
);
// Send request to the Lattice
const resp = await request({ url, payload: msg });
if (resp.length !== szs.payload.response.connect - 1) {
throw new Error('Wrong Lattice response message size.');
}
return resp;
}
/**
* Build an encrypted secure request using raw data,
* then send that request to the target Lattice, handle
* the response, and return the *decrypted* response
* payload data.
* Also updates ephemeral public key in the client.
* This is a wrapper around several local util functions.
* @param data - Unencrypted raw calldata for function
* @param requestType - Type of encrypted reques to make
* @return {Buffer} Decrypted response data (excluding metadata)
*/
export async function encryptedSecureRequest({
data,
requestType,
sharedSecret,
ephemeralPub,
url,
timeout,
}: {
data: Buffer;
requestType: LatticeSecureEncryptedRequestType;
sharedSecret: Buffer;
ephemeralPub: KeyPair;
url: string;
timeout?: number;
}): Promise<DecryptedResponse> {
// Generate a random message id for internal tracking
// of this specific request (internal on both sides).
const msgId = randomBytes(4);
// Serialize the request data into encrypted request
// payload data.
const payloadData = serializeSecureRequestEncryptedPayloadData({
data,
requestType,
ephemeralPub,
sharedSecret,
});
// Serialize the payload data into an encrypted secure
// request message.
const msg = serializeSecureRequestMsg(
msgId,
LatticeSecureMsgType.encrypted,
payloadData,
);
// Send request to Lattice
const resp = await request({
url,
payload: msg,
timeout,
});
// Deserialize the response payload data
if (resp.length !== szs.payload.response.encrypted - 1) {
throw new Error('Wrong Lattice response message size.');
}
const encPayloadData = resp.slice(
0,
szs.data.response.encrypted.encryptedData,
);
// Return decrypted response payload data
return decryptEncryptedLatticeResponseData({
encPayloadData,
requestType,
sharedSecret,
});
}
/**
* @internal
* Serialize a Secure Request message for the Lattice.
* All outgoing SDK requests are of this form.
* @param msgId - Random 4 bytes of data for internally tracking this message
* @param secureRequestType - 0x01 for connect, 0x02 for encrypted
* @param payloadData - Request data
* @return {Buffer} Serialized message to be sent to Lattice
*/
function serializeSecureRequestMsg(
msgId: Buffer,
secureRequestType: LatticeSecureMsgType,
payloadData: Buffer,
): Buffer {
// Sanity check request data
if (msgId.length !== 4) {
throw new Error('msgId must be four bytes');
}
if (
secureRequestType !== LatticeSecureMsgType.connect &&
secureRequestType !== LatticeSecureMsgType.encrypted
) {
throw new Error('Invalid Lattice secure request type');
}
// Validate the incoming payload data size. Note that the payload
// data is prepended with a secure request type byte, so the
// payload data size is one less than the expected size.
const isValidConnectPayloadDataSz =
secureRequestType === LatticeSecureMsgType.connect &&
payloadData.length === szs.payload.request.connect - 1;
const isValidEncryptedPayloadDataSz =
secureRequestType === LatticeSecureMsgType.encrypted &&
payloadData.length === szs.payload.request.encrypted - 1;
// Build payload and size
let msgSz = msgSizes.header + msgSizes.checksum;
let payloadLen;
const payload: LatticeSecureRequestPayload = {
requestType: secureRequestType,
data: payloadData,
};
if (isValidConnectPayloadDataSz) {
payloadLen = szs.payload.request.connect;
} else if (isValidEncryptedPayloadDataSz) {
payloadLen = szs.payload.request.encrypted;
} else {
throw new Error('Invalid Lattice secure request payload size');
}
msgSz += payloadLen;
// Construct the request in object form
const header: LatticeMessageHeader = {
version: LatticeProtocolVersion.v1,
type: LatticeMsgType.secure,
id: msgId,
len: payloadLen,
};
const req: LatticeSecureRequest = {
header,
payload,
};
// Now serialize the whole message
// Header | requestType | payloadData | checksum
const msg = Buffer.alloc(msgSz);
let off = 0;
// Header
msg.writeUInt8(req.header.version, off);
off += 1;
msg.writeUInt8(req.header.type, off);
off += 1;
req.header.id.copy(msg, off);
off += req.header.id.length;
msg.writeUInt16BE(req.header.len, off);
off += 2;
// Payload
msg.writeUInt8(req.payload.requestType, off);
off += 1;
req.payload.data.copy(msg, off);
off += req.payload.data.length;
// Checksum
msg.writeUInt32BE(checksum(msg.slice(0, off)), off);
off += 4;
if (off !== msgSz) {
throw new Error('Failed to build request message');
}
// We have our serialized secure message!
return msg;
}
/**
* @internal
* Serialize payload data for a Lattice secure request: connect
* @return {Buffer} - 1700 bytes, of which only 65 are used
*/
function serializeSecureRequestConnectPayloadData(
payloadData: LatticeSecureConnectRequestPayloadData,
): Buffer {
const serPayloadData = Buffer.alloc(szs.data.request.connect);
payloadData.pubkey.copy(serPayloadData, 0);
return serPayloadData;
}
/**
* @internal
* Serialize payload data for Lattice secure request: encrypted
* @param data - Raw (unencrypted) request data
* @return {Buffer} - 1700 bytes, all of which should be used
*/
function serializeSecureRequestEncryptedPayloadData({
data,
requestType,
ephemeralPub,
sharedSecret,
}: {
data: Buffer;
requestType: LatticeSecureEncryptedRequestType;
ephemeralPub: KeyPair;
sharedSecret: Buffer;
}): Buffer {
// Sanity checks request size
if (data.length > szs.data.request.encrypted.encryptedData) {
throw new Error('Encrypted request data too large');
}
// Make sure we have a shared secret. An error will be thrown
// if there is no ephemeral pub, indicating we need to reconnect.
validateEphemeralPub(ephemeralPub);
// Validate the request data size matches the desired request
const requestDataSize = szs.data.request.encrypted[requestType];
if (data.length !== requestDataSize) {
throw new Error(
`Invalid request datasize (wanted ${requestDataSize}, got ${data.length})`,
);
}
// Build the pre-encrypted data payload, which variable sized and of form:
// encryptedRequestType | data | checksum
const preEncryptedData = Buffer.alloc(1 + requestDataSize);
preEncryptedData[0] = requestType;
data.copy(preEncryptedData, 1);
const preEncryptedDataChecksum = checksum(preEncryptedData);
// Encrypt the data into a fixed size buffer. The buffer size should
// equal to the full message request less the 4-byte ephemeral id.
const _encryptedData = Buffer.alloc(szs.data.request.encrypted.encryptedData);
preEncryptedData.copy(_encryptedData, 0);
_encryptedData.writeUInt32LE(
preEncryptedDataChecksum,
preEncryptedData.length,
);
const encryptedData = aes256_encrypt(_encryptedData, sharedSecret);
// Calculate ephemeral ID
const ephemeralId = getEphemeralId(sharedSecret);
// Now we will serialize the payload data.
const serPayloadData = Buffer.alloc(szs.payload.request.encrypted - 1);
serPayloadData.writeUInt32LE(ephemeralId);
encryptedData.copy(serPayloadData, 4);
return serPayloadData;
}
/**
* @internal
* Decrypt the response data from an encrypted request.
* @param encPayloadData - Encrypted payload data in response
* @return {Buffer} Decrypted response data (excluding metadata)
*/
function decryptEncryptedLatticeResponseData({
encPayloadData,
requestType,
sharedSecret,
}: {
encPayloadData: Buffer;
requestType: LatticeSecureEncryptedRequestType;
sharedSecret: Buffer;
}) {
// Decrypt data using the *current* shared secret
const decData = aes256_decrypt(encPayloadData, sharedSecret);
// Bulid the object
const ephemeralPubSz = 65; // secp256r1 pubkey
const checksumOffset =
ephemeralPubSz + szs.data.response.encrypted[requestType];
const respData: LatticeSecureDecryptedResponse = {
ephemeralPub: decData.slice(0, ephemeralPubSz),
data: decData.slice(ephemeralPubSz, checksumOffset),
checksum: decData.readUInt32BE(checksumOffset),
};
// Validate the checksum
const validChecksum = checksum(decData.slice(0, checksumOffset));
if (respData.checksum !== validChecksum) {
throw new Error('Checksum mismatch in decrypted Lattice data');
}
// Validate the response data size
const validSz = szs.data.response.encrypted[requestType];
if (respData.data.length !== validSz) {
throw new Error('Incorrect response data returned from Lattice');
}
const newEphemeralPub = getP256KeyPairFromPub(respData.ephemeralPub);
// Returned the decrypted data
return { decryptedData: respData.data, newEphemeralPub };
}