kokio-sdk 0.0.4 → 0.0.6

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.
@@ -1,8 +1,94 @@
1
1
  import { assertActivityCompleted, assertNonNull, TurnkeyClient, TActivityResponse } from "@turnkey/http"
2
- import { Address, hashMessage, hashTypedData, SignableMessage, toHex, TypedData, TypedDataDefinition } from "viem";
3
- import { decodeAttestationObject, decodeClientDataJSON, parseAuthenticatorData } from "@simplewebauthn/server/helpers";
4
- import { base64UrlToBuffer } from "../utils.js";
2
+ import { Address, hashMessage, hashTypedData, Hex, hexToBytes, SignableMessage, toHex, TypedData, TypedDataDefinition, bytesToHex } from "viem";
3
+ import { decodeAttestationObject, decodeClientDataJSON, isoBase64URL, parseAuthenticatorData } from "@simplewebauthn/server/helpers";
4
+ import { _add0x, base64UrlToBuffer, parseSignature } from "../utils.js";
5
5
  import { WebAuthnSignature } from "../../types.js";
6
+ import { Passkey, PasskeyGetResult } from "react-native-passkey";
7
+ import { p256 } from "@noble/curves/p256";
8
+ import { AuthenticatorTransport } from "@turnkey/react-native-passkey-stamper";
9
+
10
+ type BrokenPasskeyGetResult = PasskeyGetResult | string;
11
+
12
+ /*
13
+ ** Stamp is client-side authentication. Since the passkeys are one the user's mobile device
14
+ ** react-native-passkey helps fetch passkey for the user (provided credentialId, rpId).
15
+ ** This is exactly how the WebAuthn.sol contract needs it to be.
16
+ */
17
+ export const _stamp = async (credentialId: string, rpId: string, payload: Hex): Promise<WebAuthnSignature> => {
18
+ const signingOptions = {
19
+ challenge: isoBase64URL.fromBuffer(hexToBytes(payload)), // Base64URL of the raw EIP-191 hash bytes
20
+ allowCredentials: [{
21
+ id: credentialId,
22
+ type: 'public-key',
23
+ transports: [AuthenticatorTransport.internal]
24
+ }],
25
+ rpId: rpId,
26
+ userVerification: "required"
27
+ };
28
+
29
+ let authenticationResult;
30
+ try {
31
+ authenticationResult = await Passkey.get(signingOptions);
32
+ } catch (e) {
33
+ console.log("Failed to get authenticationResult");
34
+ console.error(JSON.stringify(e, Object.getOwnPropertyNames(e)))
35
+ }
36
+
37
+ // See https://github.com/f-23/react-native-passkey/issues/54
38
+ // On Android the typedef lies. Authentication result is actually a string!
39
+ // TODO: remove me once the above is resolved.
40
+ const brokenAuthenticationResult =
41
+ authenticationResult as BrokenPasskeyGetResult;
42
+ if (typeof brokenAuthenticationResult === "string") {
43
+ authenticationResult = JSON.parse(brokenAuthenticationResult);
44
+ }
45
+
46
+ const { clientDataJSON, authenticatorData, signature } = authenticationResult.response;
47
+
48
+ // 1. Decode clientDataJSON
49
+ const clientDataJSONBuffer = isoBase64URL.toBuffer(clientDataJSON);
50
+ const clientDataJSONString = new TextDecoder().decode(clientDataJSONBuffer);
51
+
52
+ // 2. Calculate indices for the contract (byte offsets)
53
+ const typeSearchString = '"type":"webauthn.get"';
54
+ const challengeSearchString = '"challenge":';
55
+
56
+ const typeIndex = clientDataJSONString.indexOf(typeSearchString);
57
+ const challengeIndex = clientDataJSONString.indexOf(challengeSearchString);
58
+
59
+ if (typeIndex === -1) {
60
+ console.warn(`Warning: Could not find type substring '${typeSearchString}' in clientDataJSON. Setting typeIndex to 0.`);
61
+ }
62
+ if (challengeIndex === -1) {
63
+ throw new Error(`Could not find challenge substring '${challengeSearchString}' in clientDataJSON for index calculation.`);
64
+ }
65
+
66
+ // 3. Decode authenticatorData
67
+ const authenticatorDataBytes = isoBase64URL.toBuffer(authenticatorData);
68
+ const authenticatorDataHex = bytesToHex(authenticatorDataBytes);
69
+
70
+ // 4. Decode signature (ASN.1 DER encoded)
71
+ const signatureBytes = isoBase64URL.toBuffer(signature);
72
+ let parsedSignature = p256.Signature.fromDER(signatureBytes);
73
+ parsedSignature = parsedSignature.normalizeS();
74
+ let r = parsedSignature.r;
75
+ let s = parsedSignature.s;
76
+
77
+ const webAuthnSig = {
78
+ authenticatorData: authenticatorDataHex,
79
+ clientDataJSON: clientDataJSONString,
80
+ challengeIndex: BigInt(challengeIndex),
81
+ typeIndex: BigInt(typeIndex),
82
+ r: r,
83
+ s: s
84
+ };
85
+
86
+ return webAuthnSig;
87
+ }
88
+
89
+ const getBytesFromPayload = (payloadHex: Hex): Uint8Array => {
90
+ return hexToBytes(payloadHex);
91
+ };
6
92
 
7
93
  export const _signMessageWithTurnkey = async (client: TurnkeyClient, organizationId: string, signWith: Address, message: SignableMessage): Promise<WebAuthnSignature> => {
8
94
 
@@ -79,90 +165,132 @@ export const _signTypedDataWithTurnkey = async <
79
165
  }
80
166
  }
81
167
 
82
- export const _stampAndSignMessageWithTurnkey = async (client: TurnkeyClient, organizationId: string, signWith: Address, message: SignableMessage): Promise<WebAuthnSignature> => {
83
-
84
- const payload = hashMessage(message);
85
-
86
- const signedRequest = await client.stampSignRawPayload({
87
- type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
88
- organizationId: organizationId,
89
- parameters: {
90
- signWith: signWith,
91
- payload: payload,
92
- encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
93
- hashFunction: "HASH_FUNCTION_NO_OP",
94
- },
95
- timestampMs: String(Date.now()), // millisecond timestamp
96
- });
168
+ export const _stampAndSignMessageWithTurnkey = async (client: TurnkeyClient, organizationId: string, signWith: Address, payload: SignableMessage): Promise<WebAuthnSignature> => {
169
+ let signedRequest;
170
+ try {
171
+ signedRequest = await client.stampSignRawPayload({
172
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
173
+ organizationId: organizationId,
174
+ parameters: {
175
+ signWith: signWith,
176
+ payload: `${payload}`,
177
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
178
+ hashFunction: "HASH_FUNCTION_NO_OP",
179
+ },
180
+ timestampMs: String(Date.now()), // millisecond timestamp
181
+ });
182
+ }
183
+ catch (e) {
184
+ console.log("SDK error with signedRequest: ", e);
185
+ }
186
+ if (!signedRequest)
187
+ throw new Error('Error: Failed to get signed request');
97
188
 
98
- if (!signedRequest) throw new Error('Error: Failed to get signed request')
99
-
100
189
  const stampedHeaderValue = JSON.parse(signedRequest.stamp.stampHeaderValue);
190
+ let response;
191
+ try {
192
+ const res = await fetch(signedRequest.url, {
193
+ method: 'POST',
194
+ headers: {
195
+ 'Content-Type': 'application/json',
196
+ [signedRequest.stamp.stampHeaderName]: signedRequest.stamp.stampHeaderValue
197
+ },
198
+ body: signedRequest.body
199
+ });
200
+ if (!res.ok) {
201
+ const errorBody = await res.text();
202
+ throw new Error(`SDK Turnkey API request failed: ${res.status} ${res.statusText} - ${errorBody}`);
203
+ }
204
+ const responseData = await res.json();
205
+ response = responseData;
206
+ }
207
+ catch (e) {
208
+ console.log("SDK error with response: ", e);
209
+ }
101
210
 
102
- const response:TActivityResponse = await client.request('/public/v1/submit/sign_raw_payload', signedRequest)
103
-
104
- if (!response || !response.activity) throw new Error('Error: Failed to sign payload');
105
-
106
- const activity = response.activity;
107
-
108
- assertActivityCompleted(activity);
109
-
110
- const authenticatorData = base64UrlToBuffer(stampedHeaderValue.authenticatorData);
211
+ const authenticatorData = isoBase64URL.toBuffer(stampedHeaderValue.authenticatorData);
111
212
  const decodedClientDataJson = decodeClientDataJSON(stampedHeaderValue.clientDataJson);
112
-
113
- const signature = assertNonNull(activity.result.signRawPayloadResult);
213
+ // `payload` here is the hex string (e.g., "0x...") that was sent to Turnkey
214
+ // const originalPayloadBytes = Buffer.from(payload.startsWith('0x') ? payload.substring(2) : payload, 'hex');
215
+ const originalPayloadBytes = getBytesFromPayload(payload as Hex);
216
+ // Replace the challenge in decodedClientDataJson with the Base64URL of the *actual bytes*
217
+ // isoBase64URL.fromBuffer from simplewebauthn/server removes padding, which is standard for WebAuthn.
218
+ decodedClientDataJson.challenge = isoBase64URL.fromBuffer(originalPayloadBytes);
219
+ const typeIndex = JSON.stringify(decodedClientDataJson).indexOf('"type":"webauthn.get"');
220
+ const challengeIndex = JSON.stringify(decodedClientDataJson).indexOf('"challenge":');
221
+ if (typeIndex === -1 || challengeIndex === -1) {
222
+ throw new Error("Could not find type or challenge in clientDataJSON");
223
+ }
224
+ const signature = assertNonNull(response.activity.result.signRawPayloadResult);
114
225
 
115
226
  return {
116
227
  authenticatorData: toHex(authenticatorData),
117
228
  clientDataJSON: JSON.stringify(decodedClientDataJson),
118
- challengeIndex: BigInt(23),
119
- typeIndex: BigInt(1),
120
- r: BigInt(signature.r),
121
- s: BigInt(signature.s)
122
- }
123
- }
229
+ challengeIndex: BigInt(challengeIndex),
230
+ typeIndex: BigInt(typeIndex),
231
+ r: BigInt(_add0x(signature.r)),
232
+ s: BigInt(_add0x(signature.s))
233
+ };
234
+ }
124
235
 
125
236
  export const _stampAndSignTypedDataWithTurnkey = async <
126
237
  const typedData extends TypedData | Record<string, unknown>,
127
238
  primaryType extends keyof typedData | "EIP712Domain" = keyof typedData
128
239
  > (client: TurnkeyClient, organizationId: string, signWith: Address, typedData: TypedDataDefinition<typedData, primaryType>): Promise<WebAuthnSignature> => {
129
-
130
240
  const payload = hashTypedData(typedData);
131
-
132
- const signedRequest = await client.stampSignRawPayload({
133
- type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
134
- organizationId: organizationId,
135
- parameters: {
136
- signWith: signWith,
137
- payload: payload,
138
- encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
139
- hashFunction: "HASH_FUNCTION_NO_OP",
140
- },
141
- timestampMs: String(Date.now()), // millisecond timestamp
142
- });
241
+ let signedRequest;
242
+ try {
243
+ signedRequest = await client.stampSignRawPayload({
244
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
245
+ organizationId: organizationId,
246
+ parameters: {
247
+ signWith: signWith,
248
+ payload: payload,
249
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
250
+ hashFunction: "HASH_FUNCTION_NO_OP",
251
+ },
252
+ timestampMs: String(Date.now()), // millisecond timestamp
253
+ });
254
+ }
255
+ catch (e) {
256
+ console.log("SDK error: stampAndSignTypedData");
257
+ console.error(e);
258
+ }
143
259
 
144
- if (!signedRequest) throw new Error('Error: Failed to get signed request')
145
-
260
+ if (!signedRequest)
261
+ throw new Error('Error: Failed to get signed request');
146
262
  const stampedHeaderValue = JSON.parse(signedRequest.stamp.stampHeaderValue);
263
+ let response;
264
+ try {
265
+ const res = await fetch(signedRequest.url, {
266
+ method: 'POST',
267
+ headers: {
268
+ 'Content-Type': 'application/json',
269
+ [signedRequest.stamp.stampHeaderName]: signedRequest.stamp.stampHeaderValue
270
+ },
271
+ body: signedRequest.body
272
+ });
273
+ if (!res.ok) {
274
+ const errorBody = await res.text();
275
+ throw new Error(`SDK Turnkey API request failed: ${res.status} ${res.statusText} - ${errorBody}`);
276
+ }
277
+ const responseData = await res.json();
278
+ response = responseData;
279
+ }
280
+ catch (e) {
281
+ console.log("SDK error with response: ", e);
282
+ }
147
283
 
148
- const response:TActivityResponse = await client.request('/public/v1/submit/sign_raw_payload', signedRequest)
149
-
150
- if (!response || !response.activity) throw new Error('Error: Failed to sign payload');
151
-
152
- const activity = response.activity;
153
-
154
- assertActivityCompleted(activity);
155
-
156
- const authenticatorData = base64UrlToBuffer(stampedHeaderValue.authenticatorData);
284
+ const authenticatorData = isoBase64URL.toBuffer(stampedHeaderValue.authenticatorData);
157
285
  const decodedClientDataJson = decodeClientDataJSON(stampedHeaderValue.clientDataJson);
158
- const signature = assertNonNull(activity.result.signRawPayloadResult);
286
+ const signature = assertNonNull(response.activity.result.signRawPayloadResult);
159
287
 
160
288
  return {
161
289
  authenticatorData: toHex(authenticatorData),
162
290
  clientDataJSON: JSON.stringify(decodedClientDataJson),
163
291
  challengeIndex: BigInt(23),
164
292
  typeIndex: BigInt(1),
165
- r: BigInt(signature.r),
166
- s: BigInt(signature.s)
167
- }
293
+ r: BigInt(_add0x(signature.r)),
294
+ s: BigInt(_add0x(signature.s))
295
+ };
168
296
  }
@@ -1,4 +1,6 @@
1
- import { Hex, isHex } from "viem";
1
+ import { Hex, isHex, toHex } from "viem";
2
+ import { AsnParser } from "@peculiar/asn1-schema";
3
+ import { ECDSASigValue } from "@peculiar/asn1-ecc";
2
4
  import { customErrors } from "./constants.js"
3
5
 
4
6
  export const _add0x = (data: Hex | string): Hex => {
@@ -84,3 +86,38 @@ export function parseDEREncodedSignature(signature: Uint8Array): {
84
86
  s: Buffer.from(s).toString("hex"),
85
87
  };
86
88
  }
89
+
90
+ export function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
91
+ return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
92
+ }
93
+
94
+ export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
95
+ let pointer = 0;
96
+ const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
97
+
98
+ const toReturn = new Uint8Array(totalLength);
99
+
100
+ arrays.forEach((arr) => {
101
+ toReturn.set(arr, pointer);
102
+ pointer += arr.length;
103
+ });
104
+
105
+ return toReturn;
106
+ }
107
+
108
+ export function parseSignature(signature: Uint8Array) {
109
+ const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
110
+ let rBytes = new Uint8Array(parsedSignature.r);
111
+ let sBytes = new Uint8Array(parsedSignature.s);
112
+ if (shouldRemoveLeadingZero(rBytes)) {
113
+ rBytes = rBytes.slice(1);
114
+ }
115
+ if (shouldRemoveLeadingZero(sBytes)) {
116
+ sBytes = sBytes.slice(1);
117
+ }
118
+ const finalSignature = concatUint8Arrays([rBytes, sBytes]);
119
+ return {
120
+ r: toHex(finalSignature.slice(0, 32)),
121
+ s: toHex(finalSignature.slice(32)),
122
+ };
123
+ }
package/src/types.ts CHANGED
@@ -3,7 +3,7 @@ import { Hex } from "viem";
3
3
  export type P256Key = [Hex, Hex];
4
4
 
5
5
  export type WebAuthnSignature = {
6
- authenticatorData: string,
6
+ authenticatorData: Hex,
7
7
  clientDataJSON: string,
8
8
  challengeIndex: bigint,
9
9
  typeIndex: bigint,