-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.js
244 lines (214 loc) · 6.4 KB
/
index.js
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
import * as u8a from 'uint8arrays'
import { joinSignature } from '@ethersproject/bytes'
import { hashMessage } from '@ethersproject/hash'
import { computeAddress, recoverAddress } from '@ethersproject/transactions'
const CMDS = {
sign: new Uint8Array([ 1 ]),
listKeys: new Uint8Array([ 2 ]),
generateKey: new Uint8Array([ 3 ]),
readStorage: new Uint8Array([ 0xd1 ]),
writeStorage: new Uint8Array([ 0xd3 ]),
}
const toHex = b => '0x' + u8a.toString(b, 'base16')
const fromHex = s => u8a.fromString(s.slice(2), 'base16')
export function ethSignMessage (message, slot, address) {
return ethSignHash(hashMessage(message), slot, address)
}
export async function ethSignHash (hash, slot, address) {
const sig = await sign(hash, slot)
const errMsg = 'Address provided does not correspond to slot: ' + slot
return ethJoinSignature(hash, sig, address, errMsg)
}
function ethJoinSignature (hash, sig, address, errMsg) {
for (let i = 0; i < 2; ++i) {
sig.recoveryParam = i
if (address.toLowerCase() === recoverAddress(hash, sig).toLowerCase()) {
break;
} else if (i === 1) {
throw new Error(errMsg)
}
}
return joinSignature(sig)
}
export async function sign (hash, slot) {
const bytes = await sendCmd(CMDS.sign, new Uint8Array([ slot ]), fromHex(hash))
return toEIP2Signature(unpackDERSignature(bytes))
}
export async function generateKey () {
const bytes = await sendCmd(CMDS.generateKey)
if (bytes[0] === 0xE1) {
if (bytes[1] === 0x06) {
throw Error('Third key was already generated.')
} else {
throw Error('Failed to generate key. Error code: ' + toHex(bytes))
}
}
}
export async function writeStorage (slot, data) {
const bytes = await sendCmd(
CMDS.writeStorage,
new Uint8Array([ slot ]),
fromHex(data)
)
throwOnStorageError(bytes)
}
export async function readStorage (slot) {
const bytes = await sendCmd(CMDS.readStorage, new Uint8Array([ slot ]))
throwOnStorageError(bytes)
return toHex(bytes)
}
function throwOnStorageError (name, bytes) {
if (bytes.length === 2 && bytes[0] === 0xe1) {
if (bytes[1] === 0x09) {
return new Uint8Array()
} else if (bytes[1] === 0x07) {
throw Error(name + ': only slot 1 and 2 are allowed')
} else if (bytes[1] === 0x08) {
throw Error(name + ': data is already stored in this slot')
} else {
throw Error(name + ': failed with error code: ' + toHex(bytes))
}
}
}
export async function listKeys () {
const bytes = await sendCmd(CMDS.listKeys)
const { key: key1, remainder: remainder1 } = readKey(bytes, 1)
const { key: key2, remainder: remainder2 } = readKey(remainder1, 2)
const keys = [ key1, key2 ]
if (remainder2.length) {
const { key: key3 } = readKey(remainder2, 3)
keys.push(key3)
}
return keys
}
function readKey (bytes, slot) {
const length = bytes[0] + 1
const key = toHex(bytes.slice(1, length))
const address = computeAddress(key)
return {
key: {
key,
slot,
address,
did: 'did:pkh:eip155:1:' + address.toLowerCase()
},
remainder: bytes.slice(length)
}
}
export function parseURLParams (params) {
if (!params) return null
const fromHexUpper = s => fromHex('0x' + s.toLowerCase())
const parsed = Object.fromEntries((new URLSearchParams(params)).entries())
// public keys
const keyBytes = fromHexUpper(parsed.static)
const { key: key1, remainder: remainder1 } = readKey(keyBytes, 1)
const { key: key2, remainder: remainder2 } = readKey(remainder1, 2)
const keys = [ key1, key2 ]
try {
const { key: key3 } = readKey(remainder2, 3)
keys.push(key3)
} catch (e) {}
// clean up key2 signature
const errMsg = 'Signature of key 2 not valid'
const challenge = '0x' + parsed.cmd.toLowerCase().slice(4, 64 + 4)
const eipSig = toEIP2Signature(unpackDERSignature(fromHexUpper(parsed.res)))
const signature = ethJoinSignature(challenge, eipSig, key2.address, errMsg)
return {
tagVersion: '0x' + parsed.v,
keys,
proof: {
challenge,
signature,
counter: parseInt(challenge.slice(2, 10), 16) // 32-bit BigEndian
},
storage: [{
slot: 1,
value: '0x' + parsed.latch1.toLowerCase()
}, {
slot: 2,
value: '0x' + parsed.latch2.toLowerCase()
}]
}
}
async function sendCmd (cmd, slot, data) {
let payload
if (!slot && !data) {
payload = cmd
}
else if (slot && !data) {
payload = u8a.concat([ cmd, slot ])
}
else if (data) {
payload = u8a.concat([ cmd, slot, data ])
}
try {
const req = {
publicKey: {
allowCredentials: [
{
id: payload,
transports: ['nfc'],
type: 'public-key',
},
],
challenge: new Uint8Array([
113, 241, 176, 49, 249, 113, 39, 237, 135, 170, 177, 61, 15, 14, 105, 236, 120, 140, 4, 41, 65, 225, 107,
63, 214, 129, 133, 223, 169, 200, 21, 88,
]),
// rpId: DOMAIN,
timeout: 60000,
userVerification: 'discouraged',
}
}
const xdd = await navigator.credentials.get(req)
return new Uint8Array(xdd.response.signature)
} catch (err) {
throw Error('Error with scan', err)
}
}
function unpackDERSignature(sig) {
const bytes = sig.values()
if (bytes.next().value !== 0x30) {
throw Error('Invalid header, ' + toHex(sig))
}
bytes.next() // ignore second byte of header
if (bytes.next().value !== 0x02) {
throw Error('Invalid header (2).')
}
let length_r = bytes.next().value
if (length_r == 33) {
bytes.next() // ignore prepended padding
length_r -= 1
}
const r = new Uint8Array(length_r)
for (let i = 0; i < length_r; ++i) {
r[i] = bytes.next().value
}
if (bytes.next().value !== 0x02) {
throw Error('Invalid header (2).')
}
let length_s = bytes.next().value
if (length_s == 33) {
bytes.next() // ignore prepended padding
length_s -= 1
}
const s = new Uint8Array(length_r)
for (let i = 0; i < length_s; ++i) {
s[i] = bytes.next().value
}
return { r, s }
}
function toEIP2Signature ({ s, r }) {
let sNew = BigInt(toHex(s))
// SECP256k1 order constant
const curveOrder = 115792089237316195423570985008687907852837564279074904382605163141518161494337n
if (sNew > curveOrder / 2n) {
// malleable signature, not compliant with Ethereum's EIP-2
// we need to flip s value in the signature
sNew = -sNew + curveOrder
}
return {
r: toHex(r),
s: '0x' + sNew.toString(16)
}
}