passkey-kit 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +31 -0
- package/.vscode/settings.json +2 -0
- package/README.md +7 -5
- package/TODO.md +1 -2
- package/cheatsheet +12 -1
- package/package.json +6 -6
- package/packages/passkey-factory-sdk/package.json +3 -3
- package/packages/passkey-factory-sdk/src/index.ts +6 -76
- package/packages/passkey-factory-sdk/types/index.d.ts +1 -70
- package/packages/passkey-kit-sdk/package.json +3 -3
- package/packages/passkey-kit-sdk/src/index.ts +23 -69
- package/packages/passkey-kit-sdk/types/index.d.ts +13 -57
- package/src/kit.ts +123 -231
- package/tsconfig.json +1 -2
- package/types/kit.d.ts +15 -12
- package/types/src/base.d.ts +9 -0
- package/types/src/index.d.ts +3 -0
- package/types/src/kit.d.ts +53 -0
- package/types/tests/index.d.ts +1 -0
package/src/kit.ts
CHANGED
|
@@ -1,51 +1,37 @@
|
|
|
1
1
|
import { Client as PasskeyClient } from 'passkey-kit-sdk'
|
|
2
|
-
import { Client as FactoryClient
|
|
3
|
-
import { Address, Networks, StrKey, hash, xdr, Transaction, SorobanRpc, Operation,
|
|
4
|
-
import { bufToBigint, bigintToBuf } from 'bigint-conversion'
|
|
2
|
+
import { Client as FactoryClient } from 'passkey-factory-sdk'
|
|
3
|
+
import { Address, Networks, StrKey, hash, xdr, Transaction, SorobanRpc, Operation, TransactionBuilder } from '@stellar/stellar-sdk'
|
|
5
4
|
import base64url from 'base64url'
|
|
6
5
|
import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
|
|
6
|
+
import type { AuthenticatorAttestationResponseJSON } from "@simplewebauthn/types"
|
|
7
7
|
import { decode } from 'cbor-x/decode'
|
|
8
8
|
import { Buffer } from 'buffer'
|
|
9
9
|
import { PasskeyBase } from './base'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- Clean up these params and the interface as a whole
|
|
13
|
-
Might put wallet activities and maybe factory as well into the root of the class vs buried inside this.wallet and this.factory
|
|
14
|
-
@Later
|
|
15
|
-
- Right now publicKey can mean a Stellar public key or a passkey public key, there should be a noted difference
|
|
16
|
-
*/
|
|
11
|
+
type GetContractIdFunction = (keyId: string) => Promise<string>;
|
|
17
12
|
|
|
18
13
|
export class PasskeyKit extends PasskeyBase {
|
|
19
14
|
public keyId: string | undefined
|
|
20
|
-
public
|
|
15
|
+
public keyExpired: boolean | undefined
|
|
21
16
|
public wallet: PasskeyClient | undefined
|
|
22
17
|
public factory: FactoryClient
|
|
23
18
|
public networkPassphrase: Networks
|
|
24
19
|
public rpcUrl: string
|
|
25
20
|
public rpc: SorobanRpc.Server
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/* NOTE
|
|
29
|
-
- Consider adding the ability to pass in a keyId and maybe even a contractId in order to preconnect to a wallet
|
|
30
|
-
If just a keyId call `connectWallet` in order to get the contractId
|
|
31
|
-
If both keyId and contractId are passed in then we can skip the connectWallet call (though we won't get the sudoKeyId in that case)
|
|
32
|
-
We don't strictly _need_ this as a dev can just call `connectWallet` after class instantiation but it might be a nice convenience
|
|
33
|
-
`connectWallet` is async making it tricky to know when the wallet is "ready". Thus I think it's better for `connectWallet` to be called externally
|
|
34
|
-
@No
|
|
35
|
-
*/
|
|
21
|
+
|
|
36
22
|
constructor(options: {
|
|
37
23
|
rpcUrl: string,
|
|
38
24
|
launchtubeUrl?: string,
|
|
39
25
|
launchtubeJwt?: string,
|
|
40
|
-
factoryContractId?: string,
|
|
41
26
|
networkPassphrase: string,
|
|
27
|
+
factoryContractId: string,
|
|
42
28
|
}) {
|
|
43
29
|
const {
|
|
44
30
|
rpcUrl,
|
|
45
31
|
launchtubeUrl,
|
|
46
32
|
launchtubeJwt,
|
|
47
|
-
factoryContractId,
|
|
48
33
|
networkPassphrase,
|
|
34
|
+
factoryContractId,
|
|
49
35
|
} = options
|
|
50
36
|
|
|
51
37
|
super({
|
|
@@ -53,21 +39,18 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
53
39
|
launchtubeJwt,
|
|
54
40
|
})
|
|
55
41
|
|
|
56
|
-
if (factoryContractId)
|
|
57
|
-
this.factoryContractId = factoryContractId
|
|
58
|
-
|
|
59
42
|
this.rpcUrl = rpcUrl
|
|
60
43
|
this.rpc = new SorobanRpc.Server(rpcUrl)
|
|
61
44
|
this.networkPassphrase = networkPassphrase as Networks
|
|
62
45
|
this.factory = new FactoryClient({
|
|
63
|
-
contractId:
|
|
46
|
+
contractId: factoryContractId,
|
|
64
47
|
networkPassphrase,
|
|
65
48
|
rpcUrl
|
|
66
49
|
})
|
|
67
50
|
}
|
|
68
51
|
|
|
69
|
-
public async createWallet(
|
|
70
|
-
const { keyId, publicKey } = await this.createKey(
|
|
52
|
+
public async createWallet(app: string, user: string) {
|
|
53
|
+
const { keyId, publicKey } = await this.createKey(app, user)
|
|
71
54
|
|
|
72
55
|
const { result, built } = await this.factory.deploy({
|
|
73
56
|
id: keyId,
|
|
@@ -89,17 +72,19 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
89
72
|
}
|
|
90
73
|
}
|
|
91
74
|
|
|
92
|
-
public async createKey(
|
|
93
|
-
const
|
|
75
|
+
public async createKey(app: string, user: string) {
|
|
76
|
+
const now = new Date()
|
|
77
|
+
const displayName = `${user} — ${now.toLocaleString()}`
|
|
78
|
+
const { id, response} = await startRegistration({
|
|
94
79
|
challenge: base64url("stellaristhebetterblockchain"),
|
|
95
80
|
rp: {
|
|
96
81
|
// id: undefined,
|
|
97
|
-
name,
|
|
82
|
+
name: app,
|
|
98
83
|
},
|
|
99
|
-
user: {
|
|
100
|
-
id: base64url(user),
|
|
101
|
-
name:
|
|
102
|
-
displayName
|
|
84
|
+
user: { // TODO there's a real danger here of overwriting a user's key if they use the same `user` name
|
|
85
|
+
id: base64url(`${user}:${now.getTime()}:${Math.random()}`),
|
|
86
|
+
name: displayName,
|
|
87
|
+
displayName
|
|
103
88
|
},
|
|
104
89
|
authenticatorSelection: {
|
|
105
90
|
requireResidentKey: false,
|
|
@@ -110,78 +95,89 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
110
95
|
attestation: "none",
|
|
111
96
|
});
|
|
112
97
|
|
|
113
|
-
if (!this.keyId)
|
|
114
|
-
this.keyId =
|
|
115
|
-
|
|
116
|
-
// If there was no keyId we're likely about to deploy a new wallet so we should set the sudoKeyId
|
|
117
|
-
if (!this.sudoKeyId)
|
|
118
|
-
this.sudoKeyId = startRegistrationResponse.id
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const { publicKeyObject } = this.getPublicKeyObject(startRegistrationResponse.response.attestationObject);
|
|
98
|
+
if (!this.keyId)
|
|
99
|
+
this.keyId = id;
|
|
122
100
|
|
|
123
|
-
const publicKey =
|
|
124
|
-
4, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
|
125
|
-
...publicKeyObject.get('-2')!,
|
|
126
|
-
...publicKeyObject.get('-3')!
|
|
127
|
-
])
|
|
101
|
+
const publicKey = this.getPublicKey(response);
|
|
128
102
|
|
|
129
103
|
return {
|
|
130
|
-
keyId: base64url.toBuffer(
|
|
104
|
+
keyId: base64url.toBuffer(id),
|
|
131
105
|
publicKey
|
|
132
106
|
}
|
|
133
107
|
}
|
|
134
108
|
|
|
135
|
-
public async connectWallet(
|
|
109
|
+
public async connectWallet(opts: {
|
|
110
|
+
keyId?: string | Uint8Array,
|
|
111
|
+
getContractId?: GetContractIdFunction
|
|
112
|
+
}) {
|
|
113
|
+
let { keyId, getContractId } = opts
|
|
114
|
+
let keyIdBuffer: Buffer
|
|
115
|
+
|
|
136
116
|
if (!keyId) {
|
|
137
|
-
const
|
|
117
|
+
const response = await startAuthentication({
|
|
138
118
|
challenge: base64url("stellaristhebetterblockchain"),
|
|
139
119
|
// rpId: undefined,
|
|
140
120
|
userVerification: "discouraged",
|
|
141
121
|
});
|
|
142
122
|
|
|
143
|
-
|
|
123
|
+
console.log(response);
|
|
124
|
+
|
|
125
|
+
keyId = response.id
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (keyId instanceof Uint8Array) {
|
|
129
|
+
keyIdBuffer = Buffer.from(keyId)
|
|
130
|
+
keyId = base64url(keyIdBuffer)
|
|
131
|
+
} else {
|
|
132
|
+
keyIdBuffer = base64url.toBuffer(keyId)
|
|
144
133
|
}
|
|
145
134
|
|
|
146
135
|
if (!this.keyId)
|
|
147
136
|
this.keyId = keyId
|
|
148
137
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// NOTE might not need this for derivation as all signers are stored in the factory and we can use that lookup as both primary and secondary
|
|
152
|
-
let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
|
|
138
|
+
// Check for the contractId on-chain as a derivation from the keyId. This is the easiest and "cheapest" check however it will only work for the initially deployed passkey if it was used as derivation
|
|
139
|
+
let contractId: string | undefined = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
|
|
153
140
|
new xdr.HashIdPreimageContractId({
|
|
154
141
|
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
155
142
|
contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
156
143
|
new xdr.ContractIdPreimageFromAddress({
|
|
157
|
-
address: Address.fromString(this.
|
|
144
|
+
address: Address.fromString(this.factory.options.contractId).toScAddress(),
|
|
158
145
|
salt: hash(keyIdBuffer),
|
|
159
146
|
})
|
|
160
147
|
)
|
|
161
148
|
})
|
|
162
149
|
).toXDR()));
|
|
163
150
|
|
|
164
|
-
let contractData: SorobanRpc.Api.LedgerEntryResult | undefined
|
|
165
|
-
|
|
166
151
|
// attempt passkey id derivation
|
|
167
152
|
try {
|
|
168
|
-
|
|
153
|
+
await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
|
|
169
154
|
}
|
|
170
155
|
// if that fails look up from the factory mapper
|
|
171
156
|
catch {
|
|
172
|
-
|
|
173
|
-
|
|
157
|
+
contractId = undefined
|
|
158
|
+
|
|
159
|
+
if (getContractId) {
|
|
160
|
+
contractId = await getContractId(keyId)
|
|
161
|
+
|
|
162
|
+
// Handle case where temporary session signer is missing on-chain (so we can queue up a re-add)
|
|
163
|
+
try {
|
|
164
|
+
await this.rpc.getContractData(contractId, xdr.ScVal.scvBytes(keyIdBuffer), SorobanRpc.Durability.Temporary)
|
|
165
|
+
// throw true
|
|
166
|
+
} catch {
|
|
167
|
+
this.keyExpired = true
|
|
168
|
+
}
|
|
169
|
+
}
|
|
174
170
|
}
|
|
175
171
|
|
|
172
|
+
if (!contractId)
|
|
173
|
+
throw new Error('No `contractId` was found')
|
|
174
|
+
|
|
176
175
|
this.wallet = new PasskeyClient({
|
|
177
176
|
contractId,
|
|
178
177
|
networkPassphrase: this.networkPassphrase,
|
|
179
178
|
rpcUrl: this.rpcUrl
|
|
180
179
|
})
|
|
181
180
|
|
|
182
|
-
// get and set the sudo signer (passing in `contractData` from the `getContractData` call above to avoid dupe requests)
|
|
183
|
-
await this.getData(contractData)
|
|
184
|
-
|
|
185
181
|
return {
|
|
186
182
|
keyId: keyIdBuffer,
|
|
187
183
|
contractId
|
|
@@ -191,7 +187,7 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
191
187
|
public async signAuthEntry(
|
|
192
188
|
entry: xdr.SorobanAuthorizationEntry,
|
|
193
189
|
options?: {
|
|
194
|
-
keyId?: 'any' |
|
|
190
|
+
keyId?: 'any' | string | Uint8Array
|
|
195
191
|
ledgersToLive?: number
|
|
196
192
|
}
|
|
197
193
|
) {
|
|
@@ -212,7 +208,6 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
212
208
|
|
|
213
209
|
const authenticationResponse = await startAuthentication(
|
|
214
210
|
keyId === 'any'
|
|
215
|
-
|| (keyId === 'sudo' && !this.sudoKeyId)
|
|
216
211
|
|| (!keyId && !this.keyId)
|
|
217
212
|
? {
|
|
218
213
|
challenge: base64url(payload),
|
|
@@ -224,11 +219,9 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
224
219
|
// rpId: undefined,
|
|
225
220
|
allowCredentials: [
|
|
226
221
|
{
|
|
227
|
-
id: keyId
|
|
228
|
-
?
|
|
229
|
-
: keyId
|
|
230
|
-
? base64url(keyId)
|
|
231
|
-
: keyId || this.keyId!,
|
|
222
|
+
id: keyId instanceof Uint8Array
|
|
223
|
+
? base64url(Buffer.from(keyId))
|
|
224
|
+
: keyId || this.keyId!,
|
|
232
225
|
type: "public-key",
|
|
233
226
|
},
|
|
234
227
|
],
|
|
@@ -236,9 +229,10 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
236
229
|
}
|
|
237
230
|
);
|
|
238
231
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
const signature = this.compactSignature(
|
|
233
|
+
base64url.toBuffer(authenticationResponse.response.signature)
|
|
234
|
+
);
|
|
235
|
+
|
|
242
236
|
credentials.signatureExpirationLedger(lastLedger + ledgersToLive)
|
|
243
237
|
credentials.signature(xdr.ScVal.scvMap([
|
|
244
238
|
new xdr.ScMapEntry({
|
|
@@ -258,13 +252,13 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
258
252
|
val: xdr.ScVal.scvBytes(signature),
|
|
259
253
|
}),
|
|
260
254
|
]))
|
|
261
|
-
|
|
255
|
+
|
|
262
256
|
return entry
|
|
263
257
|
}
|
|
264
258
|
public async signAuthEntries(
|
|
265
259
|
entries: xdr.SorobanAuthorizationEntry[],
|
|
266
260
|
options?: {
|
|
267
|
-
keyId?: 'any' |
|
|
261
|
+
keyId?: 'any' | string | Uint8Array
|
|
268
262
|
ledgersToLive?: number
|
|
269
263
|
}
|
|
270
264
|
) {
|
|
@@ -281,21 +275,14 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
281
275
|
|
|
282
276
|
return entries
|
|
283
277
|
}
|
|
284
|
-
/* NOTE
|
|
285
|
-
- Is there anything to be done with using the TS bindings `signAuthEntries` logic? Likely not but worth exploring
|
|
286
|
-
Perhaps not but maybe the js-sdk's `authorizeEntry` or `authorizeInvocation`
|
|
287
|
-
https://stellar.github.io/js-stellar-sdk/global.html#authorizeInvocation
|
|
288
|
-
These methods all only support ed25519 Keypairs
|
|
289
|
-
@No
|
|
290
|
-
*/
|
|
291
278
|
public async sign(
|
|
292
279
|
txn: Transaction | string,
|
|
293
280
|
options?: {
|
|
294
|
-
keyId?: 'any' |
|
|
281
|
+
keyId?: 'any' | string | Uint8Array
|
|
295
282
|
ledgersToLive?: number
|
|
296
283
|
}
|
|
297
284
|
) {
|
|
298
|
-
/*
|
|
285
|
+
/*
|
|
299
286
|
- Hack to ensure we don't stack fees when simulating and assembling multiple times
|
|
300
287
|
AssembleTransaction always adds the resource fee onto the transaction fee.
|
|
301
288
|
This is bad in cases where you need to simulate multiple times
|
|
@@ -315,7 +302,7 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
315
302
|
op.type !== 'invokeHostFunction'
|
|
316
303
|
&& op.type !== 'extendFootprintTtl'
|
|
317
304
|
&& op.type !== 'restoreFootprint'
|
|
318
|
-
) throw new Error('Must include only one operation of type `invokeHostFunction` or `extendFootprintTtl` or `restoreFootprint`')
|
|
305
|
+
) throw new Error('Must include only one operation of type `invokeHostFunction` or `extendFootprintTtl` or `restoreFootprint`')
|
|
319
306
|
}
|
|
320
307
|
|
|
321
308
|
// Only need to sign auth for `invokeHostFunction` operations
|
|
@@ -336,167 +323,72 @@ export class PasskeyKit extends PasskeyBase {
|
|
|
336
323
|
return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
|
|
337
324
|
}
|
|
338
325
|
|
|
339
|
-
public async getData(contractData?: SorobanRpc.Api.LedgerEntryResult) {
|
|
340
|
-
const data: Map<string, any> = new Map()
|
|
341
|
-
|
|
342
|
-
/* TODO
|
|
343
|
-
- Pretty easy to get into a state where there is no contractId and this we error at `this.wallet!.options.contractId,`
|
|
344
|
-
We should have a better error or maybe just handle no-wallet scenarios more gracefully
|
|
345
|
-
*/
|
|
346
|
-
const { val } = contractData || await this.rpc.getContractData(
|
|
347
|
-
this.wallet!.options.contractId,
|
|
348
|
-
xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
val.contractData()
|
|
352
|
-
.val()
|
|
353
|
-
.instance()
|
|
354
|
-
.storage()
|
|
355
|
-
?.forEach((entry) => {
|
|
356
|
-
data.set(
|
|
357
|
-
scValToNative(entry.key()),
|
|
358
|
-
scValToNative(entry.val()),
|
|
359
|
-
);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
this.sudoKeyId = base64url(data.get('sudo_sig'))
|
|
363
|
-
|
|
364
|
-
return data
|
|
365
|
-
}
|
|
366
|
-
|
|
367
326
|
/* TODO
|
|
368
327
|
- Add a getKeyInfo action to get info about a specific passkey
|
|
369
328
|
Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
|
|
370
329
|
@Later
|
|
371
330
|
*/
|
|
372
331
|
|
|
373
|
-
private
|
|
374
|
-
|
|
375
|
-
const authDataUint8Array = new Uint8Array(authData);
|
|
376
|
-
const authDataView = new DataView(authDataUint8Array.buffer, 0, authDataUint8Array.length);
|
|
377
|
-
|
|
378
|
-
let offset = 0;
|
|
379
|
-
|
|
380
|
-
// RP ID Hash (32 bytes)
|
|
381
|
-
const rpIdHash = authData.slice(offset, offset + 32);
|
|
382
|
-
offset += 32;
|
|
383
|
-
|
|
384
|
-
// Flags (1 byte)
|
|
385
|
-
const flags = authDataView.getUint8(offset);
|
|
386
|
-
offset += 1;
|
|
387
|
-
|
|
388
|
-
// Sign Count (4 bytes, big endian)
|
|
389
|
-
const signCount = authDataView.getUint32(offset, false);
|
|
390
|
-
offset += 4;
|
|
391
|
-
|
|
392
|
-
// Attested Credential Data, if present
|
|
393
|
-
if (flags & 0x40) { // Checking the AT flag
|
|
394
|
-
// AAGUID (16 bytes)
|
|
395
|
-
const aaguid = authData.slice(offset, offset + 16);
|
|
396
|
-
offset += 16;
|
|
397
|
-
|
|
398
|
-
// Credential ID Length (2 bytes, big endian)
|
|
399
|
-
const credIdLength = authDataView.getUint16(offset, false);
|
|
400
|
-
offset += 2;
|
|
401
|
-
|
|
402
|
-
// Credential ID (variable length)
|
|
403
|
-
const credentialId = authData.slice(offset, offset + credIdLength);
|
|
404
|
-
offset += credIdLength;
|
|
405
|
-
|
|
406
|
-
// Credential Public Key - (77 bytes...I hope)
|
|
407
|
-
const credentialPublicKey = authData.slice(offset, offset + 77);
|
|
408
|
-
offset += 77;
|
|
409
|
-
|
|
410
|
-
// Any leftover bytes. I found some when using a YubiKey
|
|
411
|
-
const theRest = authData.slice(offset);
|
|
412
|
-
|
|
413
|
-
// Decode the credential public key to COSE
|
|
414
|
-
const publicKeyObject = new Map<string, any>(Object.entries(decode(credentialPublicKey)));
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
rpIdHash,
|
|
418
|
-
flags,
|
|
419
|
-
signCount,
|
|
420
|
-
aaguid,
|
|
421
|
-
credIdLength,
|
|
422
|
-
credentialId,
|
|
423
|
-
credentialPublicKey,
|
|
424
|
-
theRest,
|
|
425
|
-
publicKeyObject
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
throw new Error("Attested credential data not present in the flags.");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
private convertEcdsaSignatureAsnToCompact(sig: Buffer) {
|
|
433
|
-
// Define the order of the curve secp256k1
|
|
434
|
-
// https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
|
|
435
|
-
const q = Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex')
|
|
436
|
-
|
|
437
|
-
// ASN Sequence
|
|
438
|
-
let offset = 0;
|
|
439
|
-
if (sig[offset] != 0x30) {
|
|
440
|
-
throw "signature is not a sequence";
|
|
441
|
-
}
|
|
442
|
-
offset += 1;
|
|
332
|
+
private getPublicKey(response: AuthenticatorAttestationResponseJSON) {
|
|
333
|
+
let publicKey: Buffer | undefined
|
|
443
334
|
|
|
444
|
-
|
|
445
|
-
|
|
335
|
+
if (response.publicKey)
|
|
336
|
+
publicKey = base64url.toBuffer(response.publicKey).slice(response.publicKey.length - 65)
|
|
446
337
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
338
|
+
if (
|
|
339
|
+
!publicKey
|
|
340
|
+
|| publicKey[0] !== 0x04
|
|
341
|
+
|| publicKey.length !== 65
|
|
342
|
+
) {
|
|
343
|
+
// Extract and decode attestation object (CBOR), slice authenticator data to get COSE-encoded public key, and decode COSE to obtain key components
|
|
344
|
+
const { authData } = response.authenticatorData ? { authData: base64url.toBuffer(response.authenticatorData) } : decode(base64url.toBuffer(response.attestationObject));
|
|
345
|
+
const credentialIdLength = (authData[53] << 8) + authData[54];
|
|
346
|
+
const publicKeyBytes = authData.slice(55 + credentialIdLength);
|
|
347
|
+
const publicKeyCose = decode(publicKeyBytes);
|
|
348
|
+
|
|
349
|
+
publicKey = Buffer.from([
|
|
350
|
+
0x04, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
|
351
|
+
...publicKeyCose['-2'],
|
|
352
|
+
...publicKeyCose['-3']
|
|
353
|
+
])
|
|
450
354
|
}
|
|
451
|
-
offset += 1;
|
|
452
355
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
356
|
+
return publicKey
|
|
357
|
+
}
|
|
358
|
+
private compactSignature(signature: Buffer) {
|
|
359
|
+
// Decode the DER signature
|
|
360
|
+
let offset = 2;
|
|
456
361
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (rLen != 33 || sig[offset] != 0x00) {
|
|
460
|
-
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
461
|
-
}
|
|
462
|
-
offset += 1;
|
|
463
|
-
}
|
|
464
|
-
const r = sig.slice(offset, offset + 32);
|
|
465
|
-
offset += 32;
|
|
362
|
+
const rLength = signature[offset + 1];
|
|
363
|
+
const r = signature.slice(offset + 2, offset + 2 + rLength);
|
|
466
364
|
|
|
467
|
-
|
|
468
|
-
if (sig[offset] != 0x02) {
|
|
469
|
-
throw "second element in sequence is not an integer";
|
|
470
|
-
}
|
|
471
|
-
offset += 1;
|
|
365
|
+
offset += 2 + rLength;
|
|
472
366
|
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
offset += 1;
|
|
367
|
+
const sLength = signature[offset + 1];
|
|
368
|
+
const s = signature.slice(offset + 2, offset + 2 + sLength);
|
|
476
369
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
481
|
-
}
|
|
482
|
-
offset += 1;
|
|
483
|
-
}
|
|
370
|
+
// Convert r and s to BigInt
|
|
371
|
+
const rBigInt = BigInt('0x' + r.toString('hex'));
|
|
372
|
+
let sBigInt = BigInt('0x' + s.toString('hex'));
|
|
484
373
|
|
|
485
|
-
|
|
374
|
+
// Ensure s is in the low-S form
|
|
375
|
+
// https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
|
|
376
|
+
// https://discord.com/channels/897514728459468821/1233048618571927693
|
|
377
|
+
// Define the order of the curve secp256r1
|
|
378
|
+
// https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
|
|
379
|
+
const n = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551');
|
|
380
|
+
const halfN = n / 2n;
|
|
486
381
|
|
|
487
|
-
|
|
382
|
+
if (sBigInt > halfN)
|
|
383
|
+
sBigInt = n - sBigInt;
|
|
488
384
|
|
|
489
|
-
|
|
385
|
+
// Convert back to buffers and ensure they are 32 bytes
|
|
386
|
+
const rPadded = Buffer.from(rBigInt.toString(16).padStart(64, '0'), 'hex');
|
|
387
|
+
const sLowS = Buffer.from(sBigInt.toString(16).padStart(64, '0'), 'hex');
|
|
490
388
|
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
// https://discord.com/channels/897514728459468821/1233048618571927693
|
|
494
|
-
if (bufToBigint(s) > ((bufToBigint(q) - BigInt(1)) / BigInt(2))) {
|
|
495
|
-
signature64 = Buffer.from([...r, ...Buffer.from(bigintToBuf(bufToBigint(q) - bufToBigint(s), true) as ArrayBuffer)]);
|
|
496
|
-
} else {
|
|
497
|
-
signature64 = Buffer.from([...r, ...s]);
|
|
498
|
-
}
|
|
389
|
+
// Concatenate r and low-s
|
|
390
|
+
const concatSignature = Buffer.concat([rPadded, sLowS]);
|
|
499
391
|
|
|
500
|
-
return
|
|
392
|
+
return concatSignature;
|
|
501
393
|
}
|
|
502
394
|
}
|
package/tsconfig.json
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
"target": "ESNext",
|
|
7
7
|
"module": "ESNext",
|
|
8
8
|
"moduleDetection": "force",
|
|
9
|
-
// "allowJs": true,
|
|
10
9
|
|
|
11
10
|
// Bundler mode
|
|
12
11
|
"moduleResolution": "node",
|
|
@@ -26,5 +25,5 @@
|
|
|
26
25
|
"noPropertyAccessFromIndexSignature": false,
|
|
27
26
|
"strictNullChecks": true,
|
|
28
27
|
},
|
|
29
|
-
"exclude": ["./demo", "./packages", "./types", "./contracts"]
|
|
28
|
+
"exclude": ["./demo", "./packages", "./types", "./contracts", "./zephyr", "./bun_tests"]
|
|
30
29
|
}
|
package/types/kit.d.ts
CHANGED
|
@@ -3,48 +3,51 @@ import { Client as FactoryClient } from 'passkey-factory-sdk';
|
|
|
3
3
|
import { Networks, xdr, Transaction, SorobanRpc } from '@stellar/stellar-sdk';
|
|
4
4
|
import { Buffer } from 'buffer';
|
|
5
5
|
import { PasskeyBase } from './base';
|
|
6
|
+
type GetContractIdFunction = (keyId: string) => Promise<string>;
|
|
6
7
|
export declare class PasskeyKit extends PasskeyBase {
|
|
7
8
|
keyId: string | undefined;
|
|
8
|
-
|
|
9
|
+
keyExpired: boolean | undefined;
|
|
9
10
|
wallet: PasskeyClient | undefined;
|
|
10
11
|
factory: FactoryClient;
|
|
11
12
|
networkPassphrase: Networks;
|
|
12
13
|
rpcUrl: string;
|
|
13
14
|
rpc: SorobanRpc.Server;
|
|
14
|
-
factoryContractId: string;
|
|
15
15
|
constructor(options: {
|
|
16
16
|
rpcUrl: string;
|
|
17
17
|
launchtubeUrl?: string;
|
|
18
18
|
launchtubeJwt?: string;
|
|
19
|
-
factoryContractId?: string;
|
|
20
19
|
networkPassphrase: string;
|
|
20
|
+
factoryContractId: string;
|
|
21
21
|
});
|
|
22
|
-
createWallet(
|
|
22
|
+
createWallet(app: string, user: string): Promise<{
|
|
23
23
|
keyId: Buffer;
|
|
24
24
|
contractId: string;
|
|
25
25
|
xdr: string;
|
|
26
26
|
}>;
|
|
27
|
-
createKey(
|
|
27
|
+
createKey(app: string, user: string): Promise<{
|
|
28
28
|
keyId: Buffer;
|
|
29
29
|
publicKey: Buffer;
|
|
30
30
|
}>;
|
|
31
|
-
connectWallet(
|
|
31
|
+
connectWallet(opts: {
|
|
32
|
+
keyId?: string | Uint8Array;
|
|
33
|
+
getContractId?: GetContractIdFunction;
|
|
34
|
+
}): Promise<{
|
|
32
35
|
keyId: Buffer;
|
|
33
36
|
contractId: string;
|
|
34
37
|
}>;
|
|
35
38
|
signAuthEntry(entry: xdr.SorobanAuthorizationEntry, options?: {
|
|
36
|
-
keyId?: 'any' |
|
|
39
|
+
keyId?: 'any' | string | Uint8Array;
|
|
37
40
|
ledgersToLive?: number;
|
|
38
41
|
}): Promise<xdr.SorobanAuthorizationEntry>;
|
|
39
42
|
signAuthEntries(entries: xdr.SorobanAuthorizationEntry[], options?: {
|
|
40
|
-
keyId?: 'any' |
|
|
43
|
+
keyId?: 'any' | string | Uint8Array;
|
|
41
44
|
ledgersToLive?: number;
|
|
42
45
|
}): Promise<xdr.SorobanAuthorizationEntry[]>;
|
|
43
46
|
sign(txn: Transaction | string, options?: {
|
|
44
|
-
keyId?: 'any' |
|
|
47
|
+
keyId?: 'any' | string | Uint8Array;
|
|
45
48
|
ledgersToLive?: number;
|
|
46
49
|
}): Promise<string>;
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
private convertEcdsaSignatureAsnToCompact;
|
|
50
|
+
private getPublicKey;
|
|
51
|
+
private compactSignature;
|
|
50
52
|
}
|
|
53
|
+
export {};
|