-
Notifications
You must be signed in to change notification settings - Fork 57
/
crypto.ts
369 lines (324 loc) · 11.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
/**
* @file Steem crypto helpers.
* @author Johan Nordberg <code@johan-nordberg.com>
* @license
* Copyright (c) 2017 Johan Nordberg. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistribution of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistribution in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* You acknowledge that this software is not designed, licensed or intended for use
* in the design, construction, operation or maintenance of any military facility.
*/
import * as assert from 'assert'
import * as bs58 from 'bs58'
import * as ByteBuffer from 'bytebuffer'
import {createHash} from 'crypto'
import * as secp256k1 from 'secp256k1'
import {VError} from 'verror'
import {DEFAULT_ADDRESS_PREFIX, DEFAULT_CHAIN_ID} from './client'
import {Types} from './steem/serializer'
import {SignedTransaction, Transaction} from './steem/transaction'
import {copy} from './utils'
/**
* Network id used in WIF-encoding.
*/
export const NETWORK_ID = Buffer.from([0x80])
/**
* Return ripemd160 hash of input.
*/
function ripemd160(input: Buffer | string): Buffer {
return createHash('ripemd160').update(input).digest()
}
/**
* Return sha256 hash of input.
*/
function sha256(input: Buffer | string): Buffer {
return createHash('sha256').update(input).digest()
}
/**
* Return 2-round sha256 hash of input.
*/
function doubleSha256(input: Buffer | string): Buffer {
return sha256(sha256(input))
}
/**
* Encode public key with bs58+ripemd160-checksum.
*/
function encodePublic(key: Buffer, prefix: string): string {
const checksum = ripemd160(key)
return prefix + bs58.encode(Buffer.concat([key, checksum.slice(0, 4)]))
}
/**
* Decode bs58+ripemd160-checksum encoded public key.
*/
function decodePublic(encodedKey: string): {key: Buffer, prefix: string} {
const prefix = encodedKey.slice(0, 3)
assert.equal(prefix.length, 3, 'public key invalid prefix')
encodedKey = encodedKey.slice(3)
const buffer: Buffer = bs58.decode(encodedKey)
const checksum = buffer.slice(-4)
const key = buffer.slice(0, -4)
const checksumVerify = ripemd160(key).slice(0, 4)
assert.deepEqual(checksumVerify, checksum, 'public key checksum mismatch')
return {key, prefix}
}
/**
* Encode bs58+doubleSha256-checksum private key.
*/
function encodePrivate(key: Buffer): string {
assert.equal(key.readUInt8(0), 0x80, 'private key network id mismatch')
const checksum = doubleSha256(key)
return bs58.encode(Buffer.concat([key, checksum.slice(0, 4)]))
}
/**
* Decode bs58+doubleSha256-checksum encoded private key.
*/
function decodePrivate(encodedKey: string): Buffer {
const buffer: Buffer = bs58.decode(encodedKey)
assert.deepEqual(buffer.slice(0, 1), NETWORK_ID, 'private key network id mismatch')
const checksum = buffer.slice(-4)
const key = buffer.slice(0, -4)
const checksumVerify = doubleSha256(key).slice(0, 4)
assert.deepEqual(checksumVerify, checksum, 'private key checksum mismatch')
return key
}
/**
* Return true if signature is canonical, otherwise false.
*/
function isCanonicalSignature(signature: Buffer): boolean {
return (
!(signature[0] & 0x80) &&
!(signature[0] === 0 && !(signature[1] & 0x80)) &&
!(signature[32] & 0x80) &&
!(signature[32] === 0 && !(signature[33] & 0x80))
)
}
/**
* ECDSA (secp256k1) public key.
*/
export class PublicKey {
/**
* Create a new instance from a WIF-encoded key.
*/
public static fromString(wif: string) {
const {key, prefix} = decodePublic(wif)
return new PublicKey(key, prefix)
}
/**
* Create a new instance.
*/
public static from(value: string | PublicKey) {
if (value instanceof PublicKey) {
return value
} else {
return PublicKey.fromString(value)
}
}
constructor(public readonly key: Buffer, public readonly prefix = DEFAULT_ADDRESS_PREFIX) {
assert(secp256k1.publicKeyVerify(key), 'invalid public key')
}
/**
* Verify a 32-byte signature.
* @param message 32-byte message to verify.
* @param signature Signature to verify.
*/
public verify(message: Buffer, signature: Signature): boolean {
return secp256k1.verify(message, signature.data, this.key)
}
/**
* Return a WIF-encoded representation of the key.
*/
public toString() {
return encodePublic(this.key, this.prefix)
}
/**
* Return JSON representation of this key, same as toString().
*/
public toJSON() {
return this.toString()
}
/**
* Used by `utils.inspect` and `console.log` in node.js.
*/
public inspect() {
return `PublicKey: ${ this.toString() }`
}
}
export type KeyRole = 'owner' | 'active' | 'posting' | 'memo'
/**
* ECDSA (secp256k1) private key.
*/
export class PrivateKey {
/**
* Convenience to create a new instance from WIF string or buffer.
*/
public static from(value: string | Buffer) {
if (typeof value === 'string') {
return PrivateKey.fromString(value)
} else {
return new PrivateKey(value)
}
}
/**
* Create a new instance from a WIF-encoded key.
*/
public static fromString(wif: string) {
return new PrivateKey(decodePrivate(wif).slice(1))
}
/**
* Create a new instance from a seed.
*/
public static fromSeed(seed: string) {
return new PrivateKey(sha256(seed))
}
/**
* Create key from username and password.
*/
public static fromLogin(username: string, password: string, role: KeyRole = 'active') {
const seed = username + role + password
return PrivateKey.fromSeed(seed)
}
constructor(private key: Buffer) {
assert(secp256k1.privateKeyVerify(key), 'invalid private key')
}
/**
* Sign message.
* @param message 32-byte message.
*/
public sign(message: Buffer): Signature {
let rv: {signature: Buffer, recovery: number}
let attempts = 0
do {
const options = {data: sha256(Buffer.concat([message, Buffer.alloc(1, ++attempts)]))}
rv = secp256k1.sign(message, this.key, options)
} while (!isCanonicalSignature(rv.signature))
return new Signature(rv.signature, rv.recovery)
}
/**
* Derive the public key for this private key.
*/
public createPublic(prefix?: string): PublicKey {
return new PublicKey(secp256k1.publicKeyCreate(this.key), prefix)
}
/**
* Return a WIF-encoded representation of the key.
*/
public toString() {
return encodePrivate(Buffer.concat([NETWORK_ID, this.key]))
}
/**
* Used by `utils.inspect` and `console.log` in node.js. Does not show the full key
* to get the full encoded key you need to explicitly call {@link toString}.
*/
public inspect() {
const key = this.toString()
return `PrivateKey: ${ key.slice(0, 6) }...${ key.slice(-6) }`
}
}
/**
* ECDSA (secp256k1) signature.
*/
export class Signature {
public static fromBuffer(buffer: Buffer) {
assert.equal(buffer.length, 65, 'invalid signature')
const recovery = buffer.readUInt8(0) - 31
const data = buffer.slice(1)
return new Signature(data, recovery)
}
public static fromString(string: string) {
return Signature.fromBuffer(Buffer.from(string, 'hex'))
}
constructor(public data: Buffer, public recovery: number) {
assert.equal(data.length, 64, 'invalid signature')
}
/**
* Recover public key from signature by providing original signed message.
* @param message 32-byte message that was used to create the signature.
*/
public recover(message: Buffer, prefix?: string) {
return new PublicKey(secp256k1.recover(message, this.data, this.recovery), prefix)
}
public toBuffer() {
const buffer = Buffer.alloc(65)
buffer.writeUInt8(this.recovery + 31, 0)
this.data.copy(buffer, 1)
return buffer
}
public toString() {
return this.toBuffer().toString('hex')
}
}
/**
* Return the sha256 transaction digest.
* @param chainId The chain id to use when creating the hash.
*/
function transactionDigest(transaction: Transaction | SignedTransaction, chainId: Buffer = DEFAULT_CHAIN_ID) {
const buffer = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN)
try {
Types.Transaction(buffer, transaction)
} catch (cause) {
throw new VError({cause, name: 'SerializationError'}, 'Unable to serialize transaction')
}
buffer.flip()
const transactionData = Buffer.from(buffer.toBuffer())
const digest = sha256(Buffer.concat([chainId, transactionData]))
return digest
}
/**
* Return copy of transaction with signature appended to signatures array.
* @param transaction Transaction to sign.
* @param keys Key(s) to sign transaction with.
* @param options Chain id and address prefix, compatible with {@link Client}.
*/
function signTransaction(
transaction: Transaction,
keys: PrivateKey | PrivateKey[],
chainId: Buffer = DEFAULT_CHAIN_ID
) {
const digest = transactionDigest(transaction, chainId)
const signedTransaction = copy(transaction) as SignedTransaction
if (!signedTransaction.signatures) {
signedTransaction.signatures = []
}
if (!Array.isArray(keys)) { keys = [keys] }
for (const key of keys) {
const signature = key.sign(digest)
signedTransaction.signatures.push(signature.toString())
}
return signedTransaction
}
/** Misc crypto utility functions. */
export const cryptoUtils = {
decodePrivate,
doubleSha256,
encodePrivate,
encodePublic,
isCanonicalSignature,
ripemd160,
sha256,
signTransaction,
transactionDigest,
}