-
Notifications
You must be signed in to change notification settings - Fork 24
/
Crypto.ts
382 lines (353 loc) · 12.1 KB
/
Crypto.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
/**
* Copyright (c) 2018-2023, BOTLabs GmbH.
*
* This source code is licensed under the BSD 4-Clause "Original" license
* found in the LICENSE file in the root directory of this source tree.
*/
/**
* Crypto provides KILT with the utility types and methods useful for cryptographic operations, such as signing/verifying, encrypting/decrypting and hashing.
*
* The utility types and methods are wrappers for existing Polkadot functions and imported throughout KILT's protocol for various cryptographic needs.
*
* @packageDocumentation
*/
import { decodeAddress, encodeAddress } from '@polkadot/keyring'
import type {
HexString,
KeyringPair,
KiltEncryptionKeypair,
KiltKeyringPair,
} from '@kiltprotocol/types'
import {
isString,
stringToU8a,
u8aConcat,
u8aToHex,
u8aToString,
u8aToU8a,
} from '@polkadot/util'
import {
blake2AsHex,
blake2AsU8a,
randomAsU8a,
signatureVerify,
} from '@polkadot/util-crypto'
import { Keyring } from '@polkadot/api'
import nacl from 'tweetnacl'
import { v4 as uuid } from 'uuid'
import jsonabc from './jsonabc.js'
import * as SDKErrors from './SDKErrors.js'
import { ss58Format } from './ss58Format.js'
export { mnemonicGenerate, mnemonicToMiniSecret } from '@polkadot/util-crypto'
export { encodeAddress, decodeAddress, u8aToHex, u8aConcat }
/**
* Creates a new public/secret box keypair from a secret.
*
* @param secret The secret.
* @returns An object containing a box `publicKey` & `secretKey` generated from the supplied secret.
*/
export function naclBoxPairFromSecret(secret: Uint8Array): nacl.BoxKeyPair {
return nacl.box.keyPair.fromSecretKey(secret)
}
/**
* Types accepted by hashing and crypto functions.
*/
export type CryptoInput = Buffer | Uint8Array | string
export type Address = string
export type EncryptedAsymmetric = {
box: Uint8Array
nonce: Uint8Array
}
export type EncryptedAsymmetricString = {
box: string
nonce: string
}
/**
* Creates a Uint8Array value from a Uint8Array, Buffer, string or hex input.
*
* @param input Input array or string. Null or undefined result in an empty array.
* @param hexAsString Whether or not a hex string is encoded as a string instead of a number.
* @returns A (possibly empty) Uint8Array.
*/
export function coToUInt8(
input: CryptoInput | null | undefined,
hexAsString = false
): Uint8Array {
if (hexAsString && isString(input)) {
return stringToU8a(input)
}
return u8aToU8a(input)
}
/**
* Signs a message.
*
* @param message String or byte array to be signed.
* @param signKeyPair KeyringPair used for signing.
* @returns Signature over message as byte array.
*/
export function sign(
message: CryptoInput,
signKeyPair: KeyringPair
): Uint8Array {
return signKeyPair.sign(coToUInt8(message), { withType: true })
}
/**
* Signs a message. Returns signature string.
*
* @param message String or byte array to be signed.
* @param signKeyPair KeyringPair used for signing.
* @returns Signature over message as hex string.
*/
export function signStr(
message: CryptoInput,
signKeyPair: KeyringPair
): string {
return u8aToHex(sign(message, signKeyPair))
}
/**
* Verifies a signature over a message.
*
* @param message Original signed message to be verified.
* @param signature Signature as hex string or byte array.
* @param addressOrPublicKey Substrate address or public key of the signer.
*/
export function verify(
message: CryptoInput,
signature: CryptoInput,
addressOrPublicKey: Address | HexString | Uint8Array
): void {
if (signatureVerify(message, signature, addressOrPublicKey).isValid !== true)
throw new SDKErrors.SignatureUnverifiableError()
}
export type BitLength = 64 | 128 | 256 | 384 | 512
/**
* Create the blake2b and return the result as an u8a with the specified `bitLength`.
*
* @param value Value to be hashed.
* @param bitLength Bit length of hash.
* @returns Blake2b hash byte array.
*/
export function hash(value: CryptoInput, bitLength?: BitLength): Uint8Array {
return blake2AsU8a(value, bitLength)
}
/**
* Create the blake2b and return the result as a hex string.
*
* @param value Value to be hashed.
* @returns Blake2b hash as hex string.
*/
export function hashStr(value: CryptoInput): HexString {
return u8aToHex(hash(value))
}
/**
* Stringifies numbers, booleans, and objects. Object keys are sorted to yield consistent hashing.
*
* @param value Object or value to be hashed.
* @returns Stringified representation of the given object.
*/
export function encodeObjectAsStr(
value: Record<string, any> | string | number | boolean
): string {
const input =
// eslint-disable-next-line no-nested-ternary
typeof value === 'object' && value !== null
? JSON.stringify(jsonabc.sortObj(value))
: // eslint-disable-next-line no-nested-ternary
typeof value === 'number' && value !== null
? value.toString()
: typeof value === 'boolean' && value !== null
? JSON.stringify(value)
: value
return input.normalize('NFC')
}
/**
* Wrapper around nacl.box. Authenticated encryption of a message for a recipient's public key.
*
* @param message String or byte array to be encrypted.
* @param publicKeyA Public key of the recipient. The owner will be able to decrypt the message.
* @param secretKeyB Private key of the sender. Necessary to authenticate the message during decryption.
* @returns Encrypted message and nonce used for encryption.
*/
export function encryptAsymmetric(
message: CryptoInput,
publicKeyA: CryptoInput,
secretKeyB: CryptoInput
): EncryptedAsymmetric {
const nonce = nacl.randomBytes(24)
const box = nacl.box(
coToUInt8(message, true),
nonce,
coToUInt8(publicKeyA),
coToUInt8(secretKeyB)
)
return { box, nonce }
}
/**
* Wrapper around nacl.box. Authenticated encryption of a message for a recipient's public key.
*
* @param message String or byte array to be encrypted.
* @param publicKeyA Public key of the recipient. The owner will be able to decrypt the message.
* @param secretKeyB Private key of the sender. Necessary to authenticate the message during decryption.
* @returns Encrypted message and nonce used for encryption as hex strings.
*/
export function encryptAsymmetricAsStr(
message: CryptoInput,
publicKeyA: CryptoInput,
secretKeyB: CryptoInput
): EncryptedAsymmetricString {
const encrypted = encryptAsymmetric(message, publicKeyA, secretKeyB)
const box = u8aToHex(encrypted.box)
const nonce = u8aToHex(encrypted.nonce)
return { box, nonce }
}
/**
* Wrapper around nacl.box.open. Authenticated decryption of an encrypted message.
*
* @param data Object containing encrypted message and nonce used for encryption.
* @param publicKeyB Public key of the sender. Necessary to authenticate the message during decryption.
* @param secretKeyA Private key of the recipient. Required for decryption.
* @returns Decrypted message or false if decryption is unsuccessful.
*/
export function decryptAsymmetric(
data: EncryptedAsymmetric | EncryptedAsymmetricString,
publicKeyB: CryptoInput,
secretKeyA: CryptoInput
): Uint8Array | false {
const decrypted = nacl.box.open(
coToUInt8(data.box),
coToUInt8(data.nonce),
coToUInt8(publicKeyB),
coToUInt8(secretKeyA)
)
return decrypted || false
}
/**
* Wrapper around nacl.box.open. Authenticated decryption of an encrypted message.
*
* @param data Object containing encrypted message and nonce used for encryption.
* @param publicKeyB Public key of the sender. Necessary to authenticate the message during decryption.
* @param secretKeyA Private key of the recipient. Required for decryption.
* @returns Decrypted message as string or false if decryption is unsuccessful.
*/
export function decryptAsymmetricAsStr(
data: EncryptedAsymmetric | EncryptedAsymmetricString,
publicKeyB: CryptoInput,
secretKeyA: CryptoInput
): string | false {
const result = decryptAsymmetric(
data,
coToUInt8(publicKeyB),
coToUInt8(secretKeyA)
)
return result !== false ? u8aToString(result) : false
}
/**
* Signature of hashing function accepted by [[hashStatements]].
*
* @param value String to be hashed.
* @param nonce Optional nonce (as string) used to obscure hashed contents.
* @returns String representation of hash.
*/
export interface Hasher {
(value: string, nonce?: string): HexString
}
/**
* Additional options for [[hashStatements]].
*/
export interface HashingOptions {
nonces?: Record<string, string>
nonceGenerator?: (key: string) => string
hasher?: Hasher
}
/**
* Default hasher for [[hashStatements]].
*
* @param value String to be hashed.
* @param nonce Optional nonce (as string) used to obscure hashed contents.
* @returns 256 bit blake2 hash as hex string.
*/
export function saltedBlake2b256(value: string, nonce = ''): HexString {
return blake2AsHex(nonce + value, 256)
}
/**
* Configurable computation of salted over an array of statements. Can be used to validate/reproduce salted hashes
* by means of an optional nonce map.
*
* @param statements An array of statement strings to be hashed.
* @param options Optional hasher arguments.
* @param options.nonces An optional map or array of nonces. If present, it should comprise all keys of `statements`, as those will be used map nonces to statements.
* @param options.nonceGenerator An optional nonce generator. Will be used if `options.nonces` is not defined to generate a (new) nonce for each statement. The statement key is passed as its first argument. If no `nonces` or `nonceGenerator` are given this function returns unsalted hashes.
* @param options.hasher The hasher to be used. Computes a hash from a statement and an optional nonce. Required but defaults to 256 bit blake2 over `${nonce}${statement}`.
* @returns An array of objects for each statement which contain a statement, its digest, salted hash and nonce.
*/
export function hashStatements(
statements: string[],
options: HashingOptions = {}
): Array<{
digest: HexString
statement: string
saltedHash: HexString
nonce: string
}> {
// apply defaults
const defaults = {
hasher: saltedBlake2b256,
nonceGenerator: () => uuid(),
}
const hasher = options.hasher || defaults.hasher
const nonceGenerator = options.nonceGenerator || defaults.nonceGenerator
// set source for nonces
const { nonces } = options
const getNonce: HashingOptions['nonceGenerator'] =
typeof nonces === 'object' ? (key) => nonces[key] : nonceGenerator
// iterate over statements to produce salted hashes
return statements.map((statement) => {
// generate unsalted digests from statements as a first step
const digest = hasher(statement)
// if nonces were passed, they would be mapped to the statement via its digest
const nonce = getNonce(digest)
// to simplify validation, the salted hash is computed over unsalted hash (nonce key) & nonce
const saltedHash = hasher(digest, nonce)
return { digest, saltedHash, nonce, statement }
})
}
/**
* Generate typed KILT blockchain keypair from a seed or random data.
*
* @param seed The keypair seed, only optional in the tests.
* @param type Optional type of the keypair.
* @returns The keypair.
*/
export function makeKeypairFromSeed<
KeyType extends KiltKeyringPair['type'] = 'ed25519'
>(seed = randomAsU8a(32), type?: KeyType): KiltKeyringPair & { type: KeyType } {
const keyring = new Keyring({ ss58Format, type })
return keyring.addFromSeed(seed) as KiltKeyringPair & { type: KeyType }
}
/**
* Generate typed KILT blockchain keypair from a polkadot keypair URI.
*
* @param uri The URI.
* @param type Optional type of the keypair.
* @returns The keypair.
*/
export function makeKeypairFromUri<
KeyType extends KiltKeyringPair['type'] = 'ed25519'
>(uri: string, type?: KeyType): KiltKeyringPair & { type: KeyType } {
const keyring = new Keyring({ ss58Format, type })
return keyring.addFromUri(uri) as KiltKeyringPair & { type: KeyType }
}
/**
* Generate from a seed a x25519 keypair to be used as DID encryption key.
*
* @param seed The keypair seed, only optional in the tests.
* @returns The keypair.
*/
export function makeEncryptionKeypairFromSeed(
seed = randomAsU8a(32)
): KiltEncryptionKeypair {
return {
...naclBoxPairFromSecret(seed),
type: 'x25519',
}
}