generated from nichoth/template-ts
/
index.ts
394 lines (346 loc) · 12.3 KB
/
index.ts
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
import { webcrypto } from 'one-webcrypto'
import { fromString, toString } from 'uint8arrays'
import {
aesGenKey, aesExportKey, rsa, importAesKey, aesEncrypt,
aesDecrypt, sha256
} from '@oddjs/odd/components/crypto/implementation/browser'
import { SymmAlg } from 'keystore-idb/types.js'
import type { Crypto } from '@oddjs/odd'
import { writeKeyToDid, DID } from '@ssc-half-light/util'
export {
aesDecrypt,
aesEncrypt
} from '@oddjs/odd/components/crypto/implementation/browser'
export interface Device {
name:string,
did:DID,
aes:string, /* the symmetric key for this account, encrypted to the
exchange key for this device */
exchange:string
}
/**
* `devices` is a map of `{ <deviceName>: Device }`, where `device.aes` is the
* AES key encrypted to this exchange key. The private side of this exchange
* key is stored only on-device, in a keystore instance.
*/
export interface Identity {
humanName:string
username:string,
rootDid:DID,
devices:Record<string, Device>
}
export const ALGORITHM = SymmAlg.AES_GCM
/**
* Create a new `identity`. This tracks a set of exchange keys by device.
* This depends on the `crypto` interface of `odd`, the keystore that
* holds the keys for your device. This creates a public record, meaning that
* we can store this anywhere, whereas the private keys are non-exportable,
* stored only on-device.
* @param crypto Fission crypto implementation for the current device
* @param {{ humanName:string }} opts The human-readable name of this identity
* @returns {Identity}
*/
export async function create (
crypto:Crypto.Implementation,
{ humanName }:{ humanName:string }
):Promise<Identity> {
const rootDid = await writeKeyToDid(crypto) // this is equal to agentDid()
const deviceName = await createDeviceName(rootDid)
// this is the private aes key for this ID
const key = await aesGenKey(ALGORITHM)
const exported = await aesExportKey(key)
const exchangeKey = await crypto.keystore.publicExchangeKey()
// i think only RSA is supported currently
const encryptedKey = toString(
await rsa.encrypt(exported, exchangeKey),
'base64pad'
)
const initialDevices:Identity['devices'] = {}
initialDevices[deviceName] = {
aes: encryptedKey,
name: deviceName,
did: rootDid,
exchange: arrToString(exchangeKey)
}
return {
username: deviceName,
humanName,
rootDid,
devices: initialDevices
}
}
/**
* A record for an encrypted message. Contains `devices`, a record like
* `{ deviceName: <encrypted key> }`
* You would decrypt the encrypted key -- message.devices[my-device-name] --
* with the device's exchange key
* Then use the decrypted key to decrypt the payload
*/
export interface EncryptedMessage {
creator:Identity, // the person who sent the message
payload:string, /* This is the message, encrypted with the symm key for
this message */
devices:Record<string, string>
}
export type CurriedEncrypt = (data:string|Uint8Array) => Promise<EncryptedMessage>
/**
* Encrypt a given message to the given set of identities.
* To decrypt this message, use your exchange key to decrypt the symm key,
* then use the symm key to decrypt the payload.
*
* This creates a new AES key each time it is called.
* @param crypto odd crypto object
* @param ids The Identities we are encrypting to
* @param data The message we want to encrypt
*/
export async function encryptTo (
creator:Identity,
ids:Identity[],
data?:string|Uint8Array
):Promise<EncryptedMessage | CurriedEncrypt> {
if (!data) {
function group (data:string|Uint8Array) {
return encryptTo(creator, ids, data) as Promise<EncryptedMessage>
}
group.groupMembers = ids
return group
}
// need to encrypt a key to each exchange key,
// then encrypt the data with the key
const key = await aesGenKey(SymmAlg.AES_GCM)
const encryptedKeys = {}
for (const id of ids.concat(creator)) {
for await (const deviceName of Object.keys(id.devices)) {
encryptedKeys[deviceName] = arrToString(
await rsa.encrypt(await aesExportKey(key),
arrFromString(id.devices[deviceName].exchange)),
)
}
}
const payload = await encryptContent(key, data)
return { payload, devices: encryptedKeys, creator }
}
/**
* @TODO
*/
export async function decryptMsg (
crypto:Crypto.Implementation,
encryptedMsg:EncryptedMessage
):Promise<string> {
const rootDid = await writeKeyToDid(crypto)
const deviceName = await createDeviceName(rootDid)
const encryptedKey = encryptedMsg.devices[deviceName]
const decryptedKey = await decryptKey(crypto, encryptedKey)
const msgBuf = fromString(encryptedMsg.payload, 'base64pad')
const decryptedMsg = await aesDecrypt(msgBuf, decryptedKey, ALGORITHM)
return toString(decryptedMsg)
}
export type Group = {
groupMembers: Identity[];
encryptedKeys: Record<string, string>;
decrypt: (
crypto:Crypto.Implementation,
group:Group,
msg:string|Uint8Array
) => Promise<string>;
(data:string|Uint8Array): Promise<string>
}
/**
* Create a group with the given AES key. This is different than `encryptTo`
* because this takes an existing key, instead of creating a new one.
* @param creator The identity that is creating this group
* @param ids An array of group members
* @param key The AES key for this group
* @returns {Promise<Group>} Return a function
* that takes a string of data and returns a string of encrypted data. Has keys
* `encryptedKeys` and `groupMemebers`. `encryptedKeys` is a map of `deviceName`
* to the encrypted AES key for this group. `groupMembers` is an array of all
* the Identities in this group.
*/
export async function group (
creator:Identity,
ids:Identity[],
key:CryptoKey
):Promise<Group> {
function _group (data:string|Uint8Array) {
return encryptContent(key, data)
}
const encryptedKeys = {}
for (const id of ids.concat(creator)) {
for await (const deviceName of Object.keys(id.devices)) {
encryptedKeys[deviceName] = arrToString(
await rsa.encrypt(await aesExportKey(key),
arrFromString(id.devices[deviceName].exchange)),
)
}
}
_group.encryptedKeys = encryptedKeys
_group.groupMembers = ids.concat([creator])
/**
* Decrypt a given message encrypted to this group
*/
_group.decrypt = async function decrypt (
crypto:Crypto.Implementation,
group:Group,
msg:string|Uint8Array
):Promise<string> {
// get the right key from the group
const did = await writeKeyToDid(crypto)
const myKey = group.encryptedKeys[await createDeviceName(did)]
const decryptedKey = await decryptKey(crypto, myKey)
const msgBuf = typeof msg === 'string' ? fromString(msg, 'base64pad') : msg
const decryptedMsg = await aesDecrypt(msgBuf, decryptedKey, ALGORITHM)
return toString(decryptedMsg)
}
return _group
}
/**
* Take data in string format, and encrypt it with the given symmetric key.
* @param key The symmetric key used to encrypt/decrypt
* @param data The text to encrypt
* @returns {string}
*/
export async function encryptContent (
key:CryptoKey,
data:string|Uint8Array
):Promise<string> {
const _data = (typeof data === 'string' ? fromString(data) : data)
// console.log('____data____', _data)
const encrypted = toString(await aesEncrypt(
_data,
key,
ALGORITHM
), 'base64pad')
// console.log('**encyrptoeddd***', encrypted)
return encrypted
}
/**
* Encrypt a given AES key to the given exchange key
* @param key The symmetric key
* @param exchangeKey The exchange key to encrypt *to*
* @returns the encrypted key, encoded as 'base64pad'
*/
export async function encryptKey (
key:CryptoKey,
exchangeKey:Uint8Array|CryptoKey
):Promise<string> {
const encryptedKey = toString(
await rsa.encrypt(await aesExportKey(key), exchangeKey),
'base64pad'
)
return encryptedKey
}
/**
* Decrypt the given encrypted key
* @param {string} encryptedKey The encrypted key, returned by `create`
* -- identity.devices[name].aes
* @param {Crypto.Implementation} crypto An instance of ODD crypto
* @returns {Promise<CryptoKey>} The symmetric key
*/
export async function decryptKey (crypto:Crypto.Implementation, encryptedKey:string)
:Promise<CryptoKey> {
const decrypted = await crypto.keystore.decrypt(
arrFromString(encryptedKey))
const key = await importAesKey(decrypted, SymmAlg.AES_GCM)
return key
}
function hasProp<K extends PropertyKey> (data: unknown, prop: K):
data is Record<K, unknown> {
return typeof data === 'object' && data != null && prop in data
}
function isCryptoKey (val:unknown):val is CryptoKey {
return (hasProp(val, 'algorithm') &&
hasProp(val, 'extractable') && hasProp(val, 'type'))
}
/**
* the existing devices are the only places that can decrypt the key
* must call `add` from an existing device
*/
/**
* Add a device to this identity. This is performed from a device that is currently
* registered. You need to get the exchange key of the new device somehow.
*
* @param {Identity} id The `Identity` instance to add to
* @param {Crypto.Implementation} crypto An instance of Fission's crypto
* @param {string} newDid The DID of the new device
* @param {Uint8Array} exchangeKey The exchange key of the new device
* @returns {Promise<Identity>} A new identity object, with the new device
*/
export async function add (
id:Identity,
crypto:Crypto.Implementation,
newDid:DID,
exchangeKey:Uint8Array|CryptoKey|string,
):Promise<Identity> {
// need to decrypt the existing AES key, then re-encrypt it to the
// new did
const existingDid = await writeKeyToDid(crypto)
const existingDeviceName = await createDeviceName(existingDid)
const secretKey = await decryptKey(crypto,
id.devices[existingDeviceName].aes)
let encryptedKey:string
let exchangeString:string
if (typeof exchangeKey === 'string') {
const key = arrFromString(exchangeKey)
encryptedKey = await encryptKey(secretKey, key)
exchangeString = exchangeKey
} else if (ArrayBuffer.isView(exchangeKey)) {
// is uint8array
encryptedKey = await encryptKey(secretKey, exchangeKey)
exchangeString = arrToString(exchangeKey)
} else if (isCryptoKey(exchangeKey)) {
// is CryptoKey
encryptedKey = await encryptKey(secretKey, exchangeKey)
exchangeString = arrToString(
new Uint8Array(await webcrypto.subtle.exportKey('raw', exchangeKey))
)
} else {
throw new Error('Exchange key should be string, uint8Array, or CryptoKey')
}
// const encrypted = await encryptKey(secretKey, _exchangeKey!)
// const encrypted = await encryptKey(secretKey, _exchangeKey!)
const newDeviceData:Identity['devices'] = {}
const name = await createDeviceName(newDid)
newDeviceData[name] = {
name,
aes: encryptedKey,
did: newDid,
exchange: exchangeString
}
const newId:Identity = {
...id,
devices: Object.assign(id.devices, newDeviceData)
}
return newId
}
export async function createDeviceName (did:DID):Promise<string> {
const normalizedDid = did.normalize('NFD')
const hashedUsername = await sha256(
new TextEncoder().encode(normalizedDid)
)
return toString(hashedUsername, 'base32').slice(0, 32)
}
/**
* Like `createDeviceName`, but can take a `crypto` object in addition to a
* DID.
* @param {DID|Crypto.Implementation} input DID string or Crypto implementation
* @returns The optimally encoded hash of the DID
*/
export async function getDeviceName (input:DID|Crypto.Implementation):Promise<string> {
if (typeof input === 'string') {
return createDeviceName(input)
}
// is crypto object
const did = await writeKeyToDid(input)
return createDeviceName(did)
}
export const arrayBuffer = {
fromString: arrFromString,
toString: arrToString
}
function arrFromString (str:string) {
return fromString(str, 'base64pad')
}
function arrToString (arr:Uint8Array) {
return toString(arr, 'base64pad')
}