@aptos-labs/ts-sdk 7.0.1 → 7.1.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/dist/account/AbstractKeylessAccount.d.ts.map +1 -1
- package/dist/account/AbstractKeylessAccount.js +3 -0
- package/dist/account/AbstractKeylessAccount.js.map +1 -1
- package/dist/account/EphemeralKeyPair.d.ts +29 -6
- package/dist/account/EphemeralKeyPair.d.ts.map +1 -1
- package/dist/account/EphemeralKeyPair.js +35 -8
- package/dist/account/EphemeralKeyPair.js.map +1 -1
- package/dist/bcs/deserializer.d.ts.map +1 -1
- package/dist/bcs/deserializer.js +15 -0
- package/dist/bcs/deserializer.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/localNode.d.ts.map +1 -1
- package/dist/cli/localNode.js +6 -0
- package/dist/cli/localNode.js.map +1 -1
- package/dist/cli/move.d.ts.map +1 -1
- package/dist/cli/move.js +8 -0
- package/dist/cli/move.js.map +1 -1
- package/dist/cli/spawnArgs.d.ts +12 -0
- package/dist/cli/spawnArgs.d.ts.map +1 -0
- package/dist/cli/spawnArgs.js +51 -0
- package/dist/cli/spawnArgs.js.map +1 -0
- package/dist/core/crypto/ed25519.d.ts +117 -4
- package/dist/core/crypto/ed25519.d.ts.map +1 -1
- package/dist/core/crypto/ed25519.js +128 -14
- package/dist/core/crypto/ed25519.js.map +1 -1
- package/dist/core/crypto/keyless.d.ts +14 -0
- package/dist/core/crypto/keyless.d.ts.map +1 -1
- package/dist/core/crypto/keyless.js +22 -3
- package/dist/core/crypto/keyless.js.map +1 -1
- package/dist/core/crypto/poseidon.js +5 -5
- package/dist/core/crypto/poseidon.js.map +1 -1
- package/dist/core/crypto/secp256k1.d.ts +123 -5
- package/dist/core/crypto/secp256k1.d.ts.map +1 -1
- package/dist/core/crypto/secp256k1.js +137 -13
- package/dist/core/crypto/secp256k1.js.map +1 -1
- package/dist/core/crypto/secp256r1.d.ts +121 -1
- package/dist/core/crypto/secp256r1.d.ts.map +1 -1
- package/dist/core/crypto/secp256r1.js +156 -9
- package/dist/core/crypto/secp256r1.js.map +1 -1
- package/dist/core/crypto/utils.d.ts +28 -1
- package/dist/core/crypto/utils.d.ts.map +1 -1
- package/dist/core/crypto/utils.js +28 -1
- package/dist/core/crypto/utils.js.map +1 -1
- package/dist/errors/index.d.ts +19 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +35 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/internal/account.d.ts +17 -0
- package/dist/internal/account.d.ts.map +1 -1
- package/dist/internal/account.js +66 -17
- package/dist/internal/account.js.map +1 -1
- package/dist/internal/keyless.d.ts.map +1 -1
- package/dist/internal/keyless.js +86 -2
- package/dist/internal/keyless.js.map +1 -1
- package/dist/internal/transaction.d.ts.map +1 -1
- package/dist/internal/transaction.js +20 -3
- package/dist/internal/transaction.js.map +1 -1
- package/dist/transactions/transactionBuilder/encryptPayload.d.ts.map +1 -1
- package/dist/transactions/transactionBuilder/encryptPayload.js +37 -38
- package/dist/transactions/transactionBuilder/encryptPayload.js.map +1 -1
- package/dist/transactions/types.d.ts +17 -13
- package/dist/transactions/types.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +16 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +29 -0
- package/dist/utils/helpers.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/account/AbstractKeylessAccount.ts +3 -0
- package/src/account/EphemeralKeyPair.ts +35 -8
- package/src/bcs/deserializer.ts +16 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/localNode.ts +7 -0
- package/src/cli/move.ts +9 -0
- package/src/cli/spawnArgs.ts +55 -0
- package/src/core/crypto/ed25519.ts +132 -15
- package/src/core/crypto/keyless.ts +22 -3
- package/src/core/crypto/poseidon.ts +5 -5
- package/src/core/crypto/secp256k1.ts +141 -13
- package/src/core/crypto/secp256r1.ts +164 -11
- package/src/core/crypto/utils.ts +28 -1
- package/src/errors/index.ts +37 -0
- package/src/internal/account.ts +73 -17
- package/src/internal/keyless.ts +88 -2
- package/src/internal/transaction.ts +22 -3
- package/src/transactions/transactionBuilder/encryptPayload.ts +51 -45
- package/src/transactions/types.ts +17 -13
- package/src/utils/helpers.ts +33 -0
- package/src/version.ts +1 -1
|
@@ -15,6 +15,8 @@ import { PublicKey, VerifySignatureAsyncArgs } from "./publicKey.js";
|
|
|
15
15
|
import { PrivateKey } from "./privateKey.js";
|
|
16
16
|
import { Signature } from "./signature.js";
|
|
17
17
|
import { AuthenticationKey } from "../authenticationKey.js";
|
|
18
|
+
import { convertSigningMessage } from "./utils.js";
|
|
19
|
+
import { TEXT_ENCODER } from "../../utils/const.js";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Represents a Secp256r1 ECDSA public key.
|
|
@@ -101,10 +103,51 @@ export class Secp256r1PublicKey extends PublicKey {
|
|
|
101
103
|
return serializer.toUint8Array();
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Verifies a signature against the exact bytes of `message`. This is the
|
|
108
|
+
* unambiguous form — the input is interpreted as raw bytes regardless of
|
|
109
|
+
* what they encode. Pair with {@link Secp256r1PrivateKey.signBytes}.
|
|
110
|
+
*
|
|
111
|
+
* The message is SHA3-256 hashed before verification (matching the
|
|
112
|
+
* Aptos-side Secp256r1 signing convention), and the signature is required
|
|
113
|
+
* to be in canonical low-S form for malleability resistance.
|
|
114
|
+
*
|
|
115
|
+
* @param args - The arguments for verification.
|
|
116
|
+
* @param args.message - The exact bytes that were signed.
|
|
117
|
+
* @param args.signature - The signature to verify.
|
|
118
|
+
* @group Implementation
|
|
119
|
+
* @category Serialization
|
|
120
|
+
*/
|
|
121
|
+
verifyBytes(args: { message: Uint8Array; signature: Signature }): boolean {
|
|
122
|
+
const { message, signature } = args;
|
|
123
|
+
const sha3Message = sha3_256(message);
|
|
124
|
+
return p256.verify(signature.toUint8Array(), sha3Message, this.toUint8Array(), { prehash: false, lowS: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verifies a signature against the UTF-8 encoding of `message`. The input
|
|
129
|
+
* is always treated as text — there is no hex/text heuristic. Pair with
|
|
130
|
+
* {@link Secp256r1PrivateKey.signText}.
|
|
131
|
+
*
|
|
132
|
+
* @param args - The arguments for verification.
|
|
133
|
+
* @param args.message - The text that was signed.
|
|
134
|
+
* @param args.signature - The signature to verify.
|
|
135
|
+
* @group Implementation
|
|
136
|
+
* @category Serialization
|
|
137
|
+
*/
|
|
138
|
+
verifyText(args: { message: string; signature: Signature }): boolean {
|
|
139
|
+
return this.verifyBytes({ message: TEXT_ENCODER.encode(args.message), signature: args.signature });
|
|
140
|
+
}
|
|
141
|
+
|
|
104
142
|
/**
|
|
105
143
|
* Verifies a Secp256r1 signature against the public key.
|
|
106
144
|
*
|
|
107
|
-
*
|
|
145
|
+
* @deprecated The polymorphic `message: HexInput` input is ambiguous — a
|
|
146
|
+
* bare even-length string of hex characters (e.g., `"cafe"`) is verified
|
|
147
|
+
* against the 2 bytes `[0xCA, 0xFE]`, not 4 UTF-8 text bytes. Use
|
|
148
|
+
* {@link verifyBytes} for `Uint8Array` input or {@link verifyText} for
|
|
149
|
+
* `string` input; both are unambiguous. See
|
|
150
|
+
* {@link convertSigningMessage} for the full legacy rule.
|
|
108
151
|
*
|
|
109
152
|
* @param args - The arguments for verifying the signature.
|
|
110
153
|
* @param args.message - The message that was signed.
|
|
@@ -114,12 +157,9 @@ export class Secp256r1PublicKey extends PublicKey {
|
|
|
114
157
|
*/
|
|
115
158
|
verifySignature(args: { message: HexInput; signature: Signature }): boolean {
|
|
116
159
|
const { message, signature } = args;
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const rawSignature = signature.toUint8Array();
|
|
121
|
-
|
|
122
|
-
return p256.verify(rawSignature, sha3Message, this.toUint8Array(), { prehash: false });
|
|
160
|
+
const messageToVerify = convertSigningMessage(message);
|
|
161
|
+
const msgBytes = Hex.fromHexInput(messageToVerify).toUint8Array();
|
|
162
|
+
return this.verifyBytes({ message: msgBytes, signature });
|
|
123
163
|
}
|
|
124
164
|
|
|
125
165
|
/**
|
|
@@ -242,6 +282,12 @@ export class Secp256r1PrivateKey extends PrivateKey {
|
|
|
242
282
|
*/
|
|
243
283
|
private readonly key: Hex;
|
|
244
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Whether the key has been cleared from memory.
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
private cleared: boolean = false;
|
|
290
|
+
|
|
245
291
|
/**
|
|
246
292
|
* Create a new PrivateKey instance from a Uint8Array or String.
|
|
247
293
|
*
|
|
@@ -268,47 +314,106 @@ export class Secp256r1PrivateKey extends PrivateKey {
|
|
|
268
314
|
* Get the private key in bytes (Uint8Array).
|
|
269
315
|
*
|
|
270
316
|
* @returns
|
|
317
|
+
* @throws Error if the private key has been cleared from memory.
|
|
271
318
|
* @group Implementation
|
|
272
319
|
* @category Serialization
|
|
273
320
|
*/
|
|
274
321
|
toUint8Array(): Uint8Array {
|
|
322
|
+
this.ensureNotCleared();
|
|
275
323
|
return this.key.toUint8Array();
|
|
276
324
|
}
|
|
277
325
|
|
|
278
326
|
/**
|
|
279
327
|
* Get the private key as a string representation.
|
|
280
328
|
*
|
|
329
|
+
* SECURITY: This produces an immutable JS string containing the key
|
|
330
|
+
* material. Strings cannot be zeroed by `clear()` (see the `clear()`
|
|
331
|
+
* JSDoc for the four classes of unreachable copies). Avoid calling this
|
|
332
|
+
* method on long-lived `Secp256r1PrivateKey` instances in processes
|
|
333
|
+
* where memory hygiene matters; prefer `toUint8Array()`, which returns
|
|
334
|
+
* a clearable `Uint8Array`.
|
|
335
|
+
*
|
|
281
336
|
* @returns string representation of the private key
|
|
337
|
+
* @throws Error if the private key has been cleared from memory.
|
|
282
338
|
* @group Implementation
|
|
283
339
|
* @category Serialization
|
|
284
340
|
*/
|
|
285
341
|
toString(): string {
|
|
342
|
+
this.ensureNotCleared();
|
|
286
343
|
return PrivateKey.formatPrivateKey(this.key.toString(), PrivateKeyVariants.Secp256r1);
|
|
287
344
|
}
|
|
288
345
|
|
|
289
346
|
/**
|
|
290
347
|
* Get the private key as a hex string with the 0x prefix.
|
|
291
348
|
*
|
|
349
|
+
* SECURITY: Same caveat as `toString()` — produces an immutable JS string
|
|
350
|
+
* containing the key material; cannot be zeroed by `clear()`.
|
|
351
|
+
*
|
|
292
352
|
* @returns string representation of the private key.
|
|
353
|
+
* @throws Error if the private key has been cleared from memory.
|
|
293
354
|
*/
|
|
294
355
|
toHexString(): string {
|
|
356
|
+
this.ensureNotCleared();
|
|
295
357
|
return this.key.toString();
|
|
296
358
|
}
|
|
297
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Sign exactly the bytes of `message`. The input is interpreted as raw
|
|
362
|
+
* bytes regardless of what they encode. Pair with
|
|
363
|
+
* {@link Secp256r1PublicKey.verifyBytes}.
|
|
364
|
+
*
|
|
365
|
+
* The message is SHA3-256 hashed before signing (matching the Aptos-side
|
|
366
|
+
* Secp256r1 signing convention).
|
|
367
|
+
*
|
|
368
|
+
* @param message - The exact bytes to sign.
|
|
369
|
+
* @returns The generated signature for the provided bytes.
|
|
370
|
+
* @throws Error if the private key has been cleared from memory.
|
|
371
|
+
* @group Implementation
|
|
372
|
+
* @category Serialization
|
|
373
|
+
*/
|
|
374
|
+
signBytes(message: Uint8Array): Secp256r1Signature {
|
|
375
|
+
this.ensureNotCleared();
|
|
376
|
+
const sha3Message = sha3_256(message);
|
|
377
|
+
const signature = p256.sign(sha3Message, this.key.toUint8Array(), { prehash: false });
|
|
378
|
+
return new Secp256r1Signature(signature);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Sign the UTF-8 encoding of `message`. The input is always treated as
|
|
383
|
+
* text — there is no hex/text heuristic. Pair with
|
|
384
|
+
* {@link Secp256r1PublicKey.verifyText}.
|
|
385
|
+
*
|
|
386
|
+
* @param message - The text to sign.
|
|
387
|
+
* @returns The generated signature for the UTF-8 bytes of the provided text.
|
|
388
|
+
* @throws Error if the private key has been cleared from memory.
|
|
389
|
+
* @group Implementation
|
|
390
|
+
* @category Serialization
|
|
391
|
+
*/
|
|
392
|
+
signText(message: string): Secp256r1Signature {
|
|
393
|
+
return this.signBytes(TEXT_ENCODER.encode(message));
|
|
394
|
+
}
|
|
395
|
+
|
|
298
396
|
/**
|
|
299
397
|
* Sign the given message with the private key.
|
|
300
398
|
* This function generates a cryptographic signature for the provided message.
|
|
301
399
|
*
|
|
400
|
+
* @deprecated The polymorphic `message: HexInput` input is ambiguous — a
|
|
401
|
+
* bare even-length string of hex characters (e.g., `"cafe"`) is signed
|
|
402
|
+
* as the 2 bytes `[0xCA, 0xFE]`, not 4 UTF-8 text bytes. Use
|
|
403
|
+
* {@link signBytes} for `Uint8Array` input or {@link signText} for
|
|
404
|
+
* `string` input; both are unambiguous. See
|
|
405
|
+
* {@link convertSigningMessage} for the full legacy rule.
|
|
406
|
+
*
|
|
302
407
|
* @param message - A message in HexInput format to be signed.
|
|
303
408
|
* @returns Signature - The generated signature for the provided message.
|
|
409
|
+
* @throws Error if the private key has been cleared from memory.
|
|
304
410
|
* @group Implementation
|
|
305
411
|
* @category Serialization
|
|
306
412
|
*/
|
|
307
413
|
sign(message: HexInput): Secp256r1Signature {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
return new Secp256r1Signature(signature);
|
|
414
|
+
const messageToSign = convertSigningMessage(message);
|
|
415
|
+
const msgBytes = Hex.fromHexInput(messageToSign).toUint8Array();
|
|
416
|
+
return this.signBytes(msgBytes);
|
|
312
417
|
}
|
|
313
418
|
|
|
314
419
|
/**
|
|
@@ -352,13 +457,61 @@ export class Secp256r1PrivateKey extends PrivateKey {
|
|
|
352
457
|
* Derive the Secp256r1PublicKey from this private key.
|
|
353
458
|
*
|
|
354
459
|
* @returns Secp256r1PublicKey The derived public key.
|
|
460
|
+
* @throws Error if the private key has been cleared from memory.
|
|
355
461
|
* @group Implementation
|
|
356
462
|
* @category Serialization
|
|
357
463
|
*/
|
|
358
464
|
publicKey(): Secp256r1PublicKey {
|
|
465
|
+
this.ensureNotCleared();
|
|
359
466
|
const bytes = p256.getPublicKey(this.key.toUint8Array(), false);
|
|
360
467
|
return new Secp256r1PublicKey(bytes);
|
|
361
468
|
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Throws if the key has already been cleared.
|
|
472
|
+
* @private
|
|
473
|
+
*/
|
|
474
|
+
private ensureNotCleared(): void {
|
|
475
|
+
if (this.cleared) {
|
|
476
|
+
throw new Error("Private key has been cleared from memory and can no longer be used");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Overwrites the underlying private-key byte buffer with random bytes and
|
|
482
|
+
* then zeros. After calling this method the key can no longer sign or
|
|
483
|
+
* derive a public key.
|
|
484
|
+
*
|
|
485
|
+
* SECURITY: This is a best-effort window-narrowing tool, NOT a true
|
|
486
|
+
* zeroization guarantee. See `Ed25519PrivateKey.clear()` for the full
|
|
487
|
+
* enumeration of JavaScript-level limits (immutable string copies, noble
|
|
488
|
+
* `BigInt` intermediates, JIT register/stack residue, GC-relocated
|
|
489
|
+
* copies). For Secp256r1 specifically, non-extractable `crypto.subtle`
|
|
490
|
+
* P-256 keys are universally supported across modern runtimes and are
|
|
491
|
+
* the architecturally-correct path for callers who need real memory
|
|
492
|
+
* hygiene; consider that alternative for new code.
|
|
493
|
+
*
|
|
494
|
+
* @group Implementation
|
|
495
|
+
* @category Serialization
|
|
496
|
+
*/
|
|
497
|
+
clear(): void {
|
|
498
|
+
if (!this.cleared) {
|
|
499
|
+
const keyBytes = this.key.toUint8Array();
|
|
500
|
+
// Multiple overwrite passes for consistency with the other private-key classes.
|
|
501
|
+
crypto.getRandomValues(keyBytes);
|
|
502
|
+
keyBytes.fill(0xff);
|
|
503
|
+
crypto.getRandomValues(keyBytes);
|
|
504
|
+
keyBytes.fill(0);
|
|
505
|
+
this.cleared = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Returns whether `clear()` has been called.
|
|
511
|
+
*/
|
|
512
|
+
isCleared(): boolean {
|
|
513
|
+
return this.cleared;
|
|
514
|
+
}
|
|
362
515
|
}
|
|
363
516
|
|
|
364
517
|
export class WebAuthnSignature extends Signature {
|
package/src/core/crypto/utils.ts
CHANGED
|
@@ -10,7 +10,34 @@ import { BaseAccountPublicKey } from "./types.js";
|
|
|
10
10
|
import { detectPublicKeyVariant } from "./anyKeyRegistry.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Normalizes a sign/verify message into a {@link HexInput} that downstream
|
|
14
|
+
* callers can pass to `Hex.fromHexInput()`.
|
|
15
|
+
*
|
|
16
|
+
* Behavior — be aware before passing a string:
|
|
17
|
+
* - `Uint8Array` → returned as-is (used as raw bytes).
|
|
18
|
+
* - String that parses as hex via `Hex.isValid()` (with or without a `0x`
|
|
19
|
+
* prefix) → returned as the original hex string, which downstream
|
|
20
|
+
* `Hex.fromHexInput()` decodes to its byte form.
|
|
21
|
+
* - Any other string → returned as the UTF-8 byte encoding of the string.
|
|
22
|
+
*
|
|
23
|
+
* **AMBIGUITY**: a bare even-length string of hex characters is *always*
|
|
24
|
+
* interpreted as hex, even when the caller intended it as text. For example:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* sign("cafe") // signs 2 bytes: [0xCA, 0xFE]
|
|
28
|
+
* sign("decade") // signs 3 bytes: [0xDE, 0xCA, 0xDE]
|
|
29
|
+
* sign("0xcafe") // signs 2 bytes: [0xCA, 0xFE] (explicit hex)
|
|
30
|
+
* sign("hello") // signs 5 bytes: UTF-8 "hello" (not valid hex)
|
|
31
|
+
* sign(new TextEncoder().encode("cafe")) // signs 4 bytes: UTF-8 "cafe"
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* If you mean *text*, pass `TextEncoder.encode(text)` or any `Uint8Array`.
|
|
35
|
+
* If you mean *hex bytes*, the most explicit form is also a `Uint8Array`
|
|
36
|
+
* (`Hex.fromHexInput("0x...").toUint8Array()`), or a string prefixed with
|
|
37
|
+
* `0x` for clarity. The heuristic is preserved as-is for backwards
|
|
38
|
+
* compatibility — changing it would silently re-interpret bytes signed by
|
|
39
|
+
* existing dApps and wallets — but new code should treat string inputs to
|
|
40
|
+
* `sign()` / `verifySignature()` as untyped and prefer `Uint8Array`.
|
|
14
41
|
*
|
|
15
42
|
* @param message a message as a string or Uint8Array
|
|
16
43
|
*
|
package/src/errors/index.ts
CHANGED
|
@@ -361,6 +361,15 @@ type AptosApiErrorOpts = {
|
|
|
361
361
|
* @param statusText - The message associated with the response status.
|
|
362
362
|
* @param data - The response data returned from the API.
|
|
363
363
|
* @param request - The original AptosRequest that triggered the error.
|
|
364
|
+
*
|
|
365
|
+
* SECURITY: `Error.message` is sanitized for `AptosApiType.PEPPER` and
|
|
366
|
+
* `AptosApiType.PROVER` so that response bodies (which can contain JWT claims
|
|
367
|
+
* or pepper-derived material) don't leak into default log/crash sinks. The
|
|
368
|
+
* `data` field, however, ALWAYS holds the raw response body — including for
|
|
369
|
+
* those sensitive API types — so callers that log or serialize
|
|
370
|
+
* `AptosApiError.data` (e.g., `JSON.stringify(error)`, Sentry's automatic
|
|
371
|
+
* field capture, custom structured loggers) must treat it accordingly. If
|
|
372
|
+
* you only need a human-readable summary, prefer `error.message`.
|
|
364
373
|
*/
|
|
365
374
|
export class AptosApiError extends Error {
|
|
366
375
|
readonly url: string;
|
|
@@ -369,6 +378,16 @@ export class AptosApiError extends Error {
|
|
|
369
378
|
|
|
370
379
|
readonly statusText: string;
|
|
371
380
|
|
|
381
|
+
/**
|
|
382
|
+
* The raw response body returned by the API.
|
|
383
|
+
*
|
|
384
|
+
* SECURITY: For `AptosApiType.PEPPER` and `AptosApiType.PROVER`, this can
|
|
385
|
+
* contain sensitive keyless-flow material (JWT claims, pepper-derived
|
|
386
|
+
* state). It is NOT redacted here — only `Error.message` is. Treat
|
|
387
|
+
* `error.data` as sensitive when handling errors from those API types,
|
|
388
|
+
* especially before passing the error to a structured logger or crash
|
|
389
|
+
* reporter.
|
|
390
|
+
*/
|
|
372
391
|
readonly data: any;
|
|
373
392
|
|
|
374
393
|
readonly request: AptosRequest;
|
|
@@ -404,6 +423,13 @@ export class AptosApiError extends Error {
|
|
|
404
423
|
* @param {AptosRequest} opts.aptosRequest - The original request made to the Aptos API.
|
|
405
424
|
* @param {AptosResponse} opts.aptosResponse - The response received from the Aptos API.
|
|
406
425
|
*/
|
|
426
|
+
// API types whose response bodies may contain keyless-account material (JWT
|
|
427
|
+
// claims, pepper-derived state). For these we exclude the body from the error
|
|
428
|
+
// message — including from the structured-error branch — so nothing about
|
|
429
|
+
// the response payload reaches the default Error.message sink. Callers that
|
|
430
|
+
// need the full body can still read it from `AptosApiError.data`.
|
|
431
|
+
const SENSITIVE_BODY_API_TYPES: ReadonlySet<AptosApiType> = new Set([AptosApiType.PEPPER, AptosApiType.PROVER]);
|
|
432
|
+
|
|
407
433
|
function deriveErrorMessage({ apiType, aptosRequest, aptosResponse }: AptosApiErrorOpts): string {
|
|
408
434
|
// extract the W3C trace_id from the response headers if it exists. Some services set this in the response, and it's useful for debugging.
|
|
409
435
|
// See https://www.w3.org/TR/trace-context/#relationship-between-the-headers .
|
|
@@ -414,6 +440,17 @@ function deriveErrorMessage({ apiType, aptosRequest, aptosResponse }: AptosApiEr
|
|
|
414
440
|
aptosResponse.url ?? aptosRequest.url
|
|
415
441
|
} ${traceIdString}failed with`;
|
|
416
442
|
|
|
443
|
+
// For sensitive API types, redact the response body in every branch below.
|
|
444
|
+
// Doing this up-front rather than per-branch ensures a Pepper/Prover
|
|
445
|
+
// response that happens to match the `{ message, error_code }` shape (or
|
|
446
|
+
// some future well-known shape) can't slip a payload through into
|
|
447
|
+
// Error.message. The full body remains on AptosApiError.data for callers
|
|
448
|
+
// that explicitly need it.
|
|
449
|
+
const isSensitive = SENSITIVE_BODY_API_TYPES.has(apiType);
|
|
450
|
+
if (isSensitive) {
|
|
451
|
+
return `${errorPrelude} status: ${aptosResponse.statusText}(code:${aptosResponse.status}) (response body redacted for ${apiType})`;
|
|
452
|
+
}
|
|
453
|
+
|
|
417
454
|
// handle graphql responses from indexer api and extract the error message of the first error
|
|
418
455
|
if (apiType === AptosApiType.INDEXER && aptosResponse.data?.errors?.[0]?.message != null) {
|
|
419
456
|
return `${errorPrelude}: ${aptosResponse.data.errors[0].message}`;
|
package/src/internal/account.ts
CHANGED
|
@@ -83,6 +83,7 @@ import { Hex } from "../core/hex.js";
|
|
|
83
83
|
import { CurrentFungibleAssetBalancesBoolExp } from "../types/generated/types.js";
|
|
84
84
|
import { getTableItem } from "./table.js";
|
|
85
85
|
import { APTOS_COIN } from "../utils/index.js";
|
|
86
|
+
import { memoizeAsync } from "../utils/memoize.js";
|
|
86
87
|
import { AptosApiError } from "../errors/index.js";
|
|
87
88
|
import { Deserializer, U8, MoveVector } from "../bcs/index.js";
|
|
88
89
|
import { generateTransaction } from "./transactionSubmission.js";
|
|
@@ -381,6 +382,45 @@ export async function lookupOriginalAccountAddress(args: {
|
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Fetches the on-chain `authentication_key` for an account address and memoizes it for ~1 hour,
|
|
387
|
+
* keyed by `(network or fullnode URL, address)`. Used by the encrypted-transaction builder to
|
|
388
|
+
* derive auth keys when the caller does not pass them explicitly. Callers that just rotated their
|
|
389
|
+
* key and need an immediate fresh read should pass the auth key explicitly instead of relying on
|
|
390
|
+
* this cache.
|
|
391
|
+
*
|
|
392
|
+
* If the address has no `0x1::account::Account` resource on chain (a brand-new account, or a light
|
|
393
|
+
* account with balance/objects but no explicit resource), returns the address bytes as the
|
|
394
|
+
* authentication key — matching the chain's account-creation convention (see
|
|
395
|
+
* [`doesAccountExistAtAddress`]). This makes encrypted-transaction builds work for not-yet-created
|
|
396
|
+
* signers (e.g., fee-payer sponsorship of an uncreated sender).
|
|
397
|
+
*/
|
|
398
|
+
export async function fetchAndCacheAuthKeyForAddress(args: {
|
|
399
|
+
aptosConfig: AptosConfig;
|
|
400
|
+
accountAddress: AccountAddressInput;
|
|
401
|
+
}): Promise<AuthenticationKey> {
|
|
402
|
+
const { aptosConfig, accountAddress } = args;
|
|
403
|
+
const address = AccountAddress.from(accountAddress);
|
|
404
|
+
const addr = address.toString();
|
|
405
|
+
const cacheKey = `auth-key-${aptosConfig.fullnode ?? aptosConfig.network}-${addr}`;
|
|
406
|
+
return memoizeAsync(
|
|
407
|
+
async () => {
|
|
408
|
+
try {
|
|
409
|
+
const info = await getInfoUtil({ aptosConfig, accountAddress: addr });
|
|
410
|
+
return new AuthenticationKey({ data: info.authentication_key });
|
|
411
|
+
} catch (err) {
|
|
412
|
+
if (err instanceof AptosApiError && err.data?.error_code === "account_not_found") {
|
|
413
|
+
// Chain convention: with no Account resource the auth key is the address itself.
|
|
414
|
+
return new AuthenticationKey({ data: address.toUint8Array() });
|
|
415
|
+
}
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
cacheKey,
|
|
420
|
+
60 * 60 * 1000,
|
|
421
|
+
)();
|
|
422
|
+
}
|
|
423
|
+
|
|
384
424
|
/**
|
|
385
425
|
* Retrieves the count of tokens owned by a specific account address.
|
|
386
426
|
*
|
|
@@ -958,17 +998,27 @@ async function doesAccountExistAtAddress(args: {
|
|
|
958
998
|
}
|
|
959
999
|
}
|
|
960
1000
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1001
|
+
// Lazy: instantiating TypeTagU8 at module-init time creates an ESM circular
|
|
1002
|
+
// import (structEnumParser → internal/account → transactions/index → typeTag)
|
|
1003
|
+
// where TypeTagU8 may not yet be a constructor when this file evaluates.
|
|
1004
|
+
// Building the ABI on first use sidesteps the order dependency.
|
|
1005
|
+
let _rotateAuthKeyAbi: EntryFunctionABI | undefined;
|
|
1006
|
+
function rotateAuthKeyAbi(): EntryFunctionABI {
|
|
1007
|
+
if (!_rotateAuthKeyAbi) {
|
|
1008
|
+
_rotateAuthKeyAbi = {
|
|
1009
|
+
typeParameters: [],
|
|
1010
|
+
parameters: [
|
|
1011
|
+
new TypeTagU8(),
|
|
1012
|
+
TypeTagVector.u8(),
|
|
1013
|
+
new TypeTagU8(),
|
|
1014
|
+
TypeTagVector.u8(),
|
|
1015
|
+
TypeTagVector.u8(),
|
|
1016
|
+
TypeTagVector.u8(),
|
|
1017
|
+
],
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return _rotateAuthKeyAbi;
|
|
1021
|
+
}
|
|
972
1022
|
|
|
973
1023
|
/**
|
|
974
1024
|
* Rotates the authentication key for a given account.
|
|
@@ -1065,16 +1115,22 @@ async function rotateAuthKeyWithChallenge(
|
|
|
1065
1115
|
MoveVector.U8(proofSignedByCurrentKey.toUint8Array()),
|
|
1066
1116
|
MoveVector.U8(proofSignedByNewKey.toUint8Array()),
|
|
1067
1117
|
],
|
|
1068
|
-
abi: rotateAuthKeyAbi,
|
|
1118
|
+
abi: rotateAuthKeyAbi(),
|
|
1069
1119
|
},
|
|
1070
1120
|
options,
|
|
1071
1121
|
});
|
|
1072
1122
|
}
|
|
1073
1123
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1124
|
+
let _rotateAuthKeyUnverifiedAbi: EntryFunctionABI | undefined;
|
|
1125
|
+
function rotateAuthKeyUnverifiedAbi(): EntryFunctionABI {
|
|
1126
|
+
if (!_rotateAuthKeyUnverifiedAbi) {
|
|
1127
|
+
_rotateAuthKeyUnverifiedAbi = {
|
|
1128
|
+
typeParameters: [],
|
|
1129
|
+
parameters: [new TypeTagU8(), TypeTagVector.u8()],
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
return _rotateAuthKeyUnverifiedAbi;
|
|
1133
|
+
}
|
|
1078
1134
|
|
|
1079
1135
|
/**
|
|
1080
1136
|
* Rotates the authentication key for a given account without verifying the new key.
|
|
@@ -1105,7 +1161,7 @@ export async function rotateAuthKeyUnverified(args: {
|
|
|
1105
1161
|
new U8(accountPublicKeyToSigningScheme(toNewPublicKey)), // to scheme
|
|
1106
1162
|
MoveVector.U8(accountPublicKeyToBaseAccountPublicKey(toNewPublicKey).toUint8Array()),
|
|
1107
1163
|
],
|
|
1108
|
-
abi: rotateAuthKeyUnverifiedAbi,
|
|
1164
|
+
abi: rotateAuthKeyUnverifiedAbi(),
|
|
1109
1165
|
},
|
|
1110
1166
|
options,
|
|
1111
1167
|
});
|
package/src/internal/keyless.ts
CHANGED
|
@@ -108,6 +108,12 @@ export async function getProof(args: {
|
|
|
108
108
|
if (Hex.fromHexInput(pepper).toUint8Array().length !== KeylessAccount.PEPPER_LENGTH) {
|
|
109
109
|
throw new Error(`Pepper needs to be ${KeylessAccount.PEPPER_LENGTH} bytes`);
|
|
110
110
|
}
|
|
111
|
+
// SECURITY: jwtDecode does NOT verify the JWT signature. The prover service
|
|
112
|
+
// is the next hop and will reject a tampered JWT, and the on-chain keyless
|
|
113
|
+
// verifier validates the signature against the JWK set published on-chain.
|
|
114
|
+
// Callers must still source `jwt` from a trusted IdP redirect flow — accepting
|
|
115
|
+
// a user-supplied JWT here will produce a useless proof, not a forged one,
|
|
116
|
+
// but it also leaks the (unverified) claims to the prover.
|
|
111
117
|
const decodedJwt = jwtDecode<JwtPayload>(jwt);
|
|
112
118
|
if (typeof decodedJwt.iat !== "number") {
|
|
113
119
|
throw new Error("iat was not found");
|
|
@@ -261,6 +267,28 @@ export async function updateFederatedKeylessJwkSetTransaction(args: {
|
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
// SSRF guard: require HTTPS. Without this check a caller-supplied `iss` or
|
|
271
|
+
// `jwksUrl` could redirect the fetch to plaintext HTTP, cloud-metadata
|
|
272
|
+
// endpoints (e.g., `http://169.254.169.254/...`), internal services, or
|
|
273
|
+
// non-network schemes like `file:` / `data:`. The on-chain JWKS update is
|
|
274
|
+
// a privileged operation, so we refuse to source key material over an
|
|
275
|
+
// untrusted transport.
|
|
276
|
+
let parsedJwksUrl: URL;
|
|
277
|
+
try {
|
|
278
|
+
parsedJwksUrl = new URL(jwksUrl);
|
|
279
|
+
} catch {
|
|
280
|
+
throw KeylessError.fromErrorType({
|
|
281
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
282
|
+
details: "JWKS URL is not a valid URL",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (parsedJwksUrl.protocol !== "https:") {
|
|
286
|
+
throw KeylessError.fromErrorType({
|
|
287
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
288
|
+
details: `JWKS URL must use https: (got ${parsedJwksUrl.protocol})`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
264
292
|
let response: Response;
|
|
265
293
|
|
|
266
294
|
try {
|
|
@@ -275,13 +303,19 @@ export async function updateFederatedKeylessJwkSetTransaction(args: {
|
|
|
275
303
|
} else {
|
|
276
304
|
errorMessage = `error unknown - ${error}`;
|
|
277
305
|
}
|
|
306
|
+
// Surface only the origin (scheme + host + port) of the JWKS URL in the
|
|
307
|
+
// user-facing error. The full URL, which may include `iss`-derived path
|
|
308
|
+
// segments or tenant identifiers from enterprise IdPs, is intentionally
|
|
309
|
+
// omitted to avoid leaking infrastructure details into logs / crash
|
|
310
|
+
// reporters.
|
|
278
311
|
throw KeylessError.fromErrorType({
|
|
279
312
|
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
280
|
-
details: `Failed to fetch JWKS
|
|
313
|
+
details: `Failed to fetch JWKS from ${parsedJwksUrl.origin}: ${errorMessage}`,
|
|
281
314
|
});
|
|
282
315
|
}
|
|
283
316
|
|
|
284
|
-
const
|
|
317
|
+
const rawJwks: unknown = await response.json();
|
|
318
|
+
const jwks = validateJwksResponse(rawJwks, parsedJwksUrl.origin);
|
|
285
319
|
return generateTransaction({
|
|
286
320
|
aptosConfig,
|
|
287
321
|
sender: sender.accountAddress,
|
|
@@ -298,3 +332,55 @@ export async function updateFederatedKeylessJwkSetTransaction(args: {
|
|
|
298
332
|
options,
|
|
299
333
|
});
|
|
300
334
|
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Caller can supply any IdP URL, so the JWKS response is untrusted. The shape
|
|
338
|
+
* isn't enforced by the TS cast above, and `jwks.keys.map(...)` would throw a
|
|
339
|
+
* confusing `TypeError: Cannot read properties of ... 'map'` on malformed
|
|
340
|
+
* payloads. Worse, a hostile/buggy IdP could return an unboundedly large
|
|
341
|
+
* `keys` array and we'd pack the whole thing into the on-chain transaction.
|
|
342
|
+
*
|
|
343
|
+
* Validate the four fields we actually use (kid, alg, e, n), cap the key
|
|
344
|
+
* count, and surface a single descriptive error when anything is off.
|
|
345
|
+
*/
|
|
346
|
+
const MAX_FEDERATED_JWKS_KEYS = 32;
|
|
347
|
+
|
|
348
|
+
function validateJwksResponse(raw: unknown, originForError: string): JWKS {
|
|
349
|
+
if (raw === null || typeof raw !== "object" || !Array.isArray((raw as { keys?: unknown }).keys)) {
|
|
350
|
+
throw KeylessError.fromErrorType({
|
|
351
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
352
|
+
details: `JWKS response from ${originForError} is missing a 'keys' array`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const keys = (raw as { keys: unknown[] }).keys;
|
|
356
|
+
if (keys.length === 0) {
|
|
357
|
+
throw KeylessError.fromErrorType({
|
|
358
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
359
|
+
details: `JWKS response from ${originForError} has an empty 'keys' array`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (keys.length > MAX_FEDERATED_JWKS_KEYS) {
|
|
363
|
+
throw KeylessError.fromErrorType({
|
|
364
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
365
|
+
details: `JWKS response from ${originForError} has ${keys.length} keys (max ${MAX_FEDERATED_JWKS_KEYS})`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
369
|
+
const key = keys[i];
|
|
370
|
+
if (key === null || typeof key !== "object") {
|
|
371
|
+
throw KeylessError.fromErrorType({
|
|
372
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
373
|
+
details: `JWKS response from ${originForError}: key at index ${i} is not an object`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
for (const field of ["kid", "alg", "e", "n"] as const) {
|
|
377
|
+
if (typeof (key as Record<string, unknown>)[field] !== "string") {
|
|
378
|
+
throw KeylessError.fromErrorType({
|
|
379
|
+
type: KeylessErrorType.JWK_FETCH_FAILED_FEDERATED,
|
|
380
|
+
details: `JWKS response from ${originForError}: key at index ${i} is missing string field '${field}'`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return raw as JWKS;
|
|
386
|
+
}
|
|
@@ -200,6 +200,16 @@ export async function waitForTransaction(args: {
|
|
|
200
200
|
let backoffIntervalMs = 200;
|
|
201
201
|
const backoffMultiplier = 1.5;
|
|
202
202
|
|
|
203
|
+
// A response is "settled" when the fullnode has populated the execution result.
|
|
204
|
+
// There is a window where `type` flips to a committed variant (e.g. User) before
|
|
205
|
+
// `success`/`vm_status` are filled in — during that window we must keep polling,
|
|
206
|
+
// not treat the partial response as a failure.
|
|
207
|
+
function isUnsettled(txn: TransactionResponse | undefined): boolean {
|
|
208
|
+
if (txn === undefined) return true;
|
|
209
|
+
if (txn.type === TransactionResponseType.Pending) return true;
|
|
210
|
+
return (txn as { success?: boolean }).success === undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
203
213
|
/**
|
|
204
214
|
* Handles API errors by throwing the last error or a timeout error for a failed transaction.
|
|
205
215
|
*
|
|
@@ -223,7 +233,7 @@ export async function waitForTransaction(args: {
|
|
|
223
233
|
// check to see if the txn is already on the blockchain
|
|
224
234
|
try {
|
|
225
235
|
lastTxn = await getTransactionByHash({ aptosConfig, transactionHash });
|
|
226
|
-
isPending = lastTxn
|
|
236
|
+
isPending = isUnsettled(lastTxn);
|
|
227
237
|
} catch (e) {
|
|
228
238
|
handleAPIError(e);
|
|
229
239
|
}
|
|
@@ -233,7 +243,7 @@ export async function waitForTransaction(args: {
|
|
|
233
243
|
const startTime = Date.now();
|
|
234
244
|
try {
|
|
235
245
|
lastTxn = await longWaitForTransaction({ aptosConfig, transactionHash });
|
|
236
|
-
isPending = lastTxn
|
|
246
|
+
isPending = isUnsettled(lastTxn);
|
|
237
247
|
} catch (e) {
|
|
238
248
|
handleAPIError(e);
|
|
239
249
|
}
|
|
@@ -248,7 +258,7 @@ export async function waitForTransaction(args: {
|
|
|
248
258
|
try {
|
|
249
259
|
lastTxn = await getTransactionByHash({ aptosConfig, transactionHash });
|
|
250
260
|
|
|
251
|
-
isPending = lastTxn
|
|
261
|
+
isPending = isUnsettled(lastTxn);
|
|
252
262
|
|
|
253
263
|
if (!isPending) {
|
|
254
264
|
break;
|
|
@@ -280,6 +290,15 @@ export async function waitForTransaction(args: {
|
|
|
280
290
|
lastTxn,
|
|
281
291
|
);
|
|
282
292
|
}
|
|
293
|
+
// If we exited the loop with a committed-shaped response that hasn't been
|
|
294
|
+
// fully populated yet (success/vm_status still undefined), this is the
|
|
295
|
+
// indexer-lag race — surface it as a timeout, not as a failed transaction.
|
|
296
|
+
if ((lastTxn as { success?: boolean }).success === undefined) {
|
|
297
|
+
throw new WaitForTransactionError(
|
|
298
|
+
`Transaction ${transactionHash} did not finish indexing within ${timeoutSecs} seconds`,
|
|
299
|
+
lastTxn,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
283
302
|
if (!checkSuccess) {
|
|
284
303
|
return lastTxn;
|
|
285
304
|
}
|