@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.
Files changed (93) hide show
  1. package/dist/account/AbstractKeylessAccount.d.ts.map +1 -1
  2. package/dist/account/AbstractKeylessAccount.js +3 -0
  3. package/dist/account/AbstractKeylessAccount.js.map +1 -1
  4. package/dist/account/EphemeralKeyPair.d.ts +29 -6
  5. package/dist/account/EphemeralKeyPair.d.ts.map +1 -1
  6. package/dist/account/EphemeralKeyPair.js +35 -8
  7. package/dist/account/EphemeralKeyPair.js.map +1 -1
  8. package/dist/bcs/deserializer.d.ts.map +1 -1
  9. package/dist/bcs/deserializer.js +15 -0
  10. package/dist/bcs/deserializer.js.map +1 -1
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/localNode.d.ts.map +1 -1
  16. package/dist/cli/localNode.js +6 -0
  17. package/dist/cli/localNode.js.map +1 -1
  18. package/dist/cli/move.d.ts.map +1 -1
  19. package/dist/cli/move.js +8 -0
  20. package/dist/cli/move.js.map +1 -1
  21. package/dist/cli/spawnArgs.d.ts +12 -0
  22. package/dist/cli/spawnArgs.d.ts.map +1 -0
  23. package/dist/cli/spawnArgs.js +51 -0
  24. package/dist/cli/spawnArgs.js.map +1 -0
  25. package/dist/core/crypto/ed25519.d.ts +117 -4
  26. package/dist/core/crypto/ed25519.d.ts.map +1 -1
  27. package/dist/core/crypto/ed25519.js +128 -14
  28. package/dist/core/crypto/ed25519.js.map +1 -1
  29. package/dist/core/crypto/keyless.d.ts +14 -0
  30. package/dist/core/crypto/keyless.d.ts.map +1 -1
  31. package/dist/core/crypto/keyless.js +22 -3
  32. package/dist/core/crypto/keyless.js.map +1 -1
  33. package/dist/core/crypto/poseidon.js +5 -5
  34. package/dist/core/crypto/poseidon.js.map +1 -1
  35. package/dist/core/crypto/secp256k1.d.ts +123 -5
  36. package/dist/core/crypto/secp256k1.d.ts.map +1 -1
  37. package/dist/core/crypto/secp256k1.js +137 -13
  38. package/dist/core/crypto/secp256k1.js.map +1 -1
  39. package/dist/core/crypto/secp256r1.d.ts +121 -1
  40. package/dist/core/crypto/secp256r1.d.ts.map +1 -1
  41. package/dist/core/crypto/secp256r1.js +156 -9
  42. package/dist/core/crypto/secp256r1.js.map +1 -1
  43. package/dist/core/crypto/utils.d.ts +28 -1
  44. package/dist/core/crypto/utils.d.ts.map +1 -1
  45. package/dist/core/crypto/utils.js +28 -1
  46. package/dist/core/crypto/utils.js.map +1 -1
  47. package/dist/errors/index.d.ts +19 -0
  48. package/dist/errors/index.d.ts.map +1 -1
  49. package/dist/errors/index.js +35 -0
  50. package/dist/errors/index.js.map +1 -1
  51. package/dist/internal/account.d.ts +17 -0
  52. package/dist/internal/account.d.ts.map +1 -1
  53. package/dist/internal/account.js +66 -17
  54. package/dist/internal/account.js.map +1 -1
  55. package/dist/internal/keyless.d.ts.map +1 -1
  56. package/dist/internal/keyless.js +86 -2
  57. package/dist/internal/keyless.js.map +1 -1
  58. package/dist/internal/transaction.d.ts.map +1 -1
  59. package/dist/internal/transaction.js +20 -3
  60. package/dist/internal/transaction.js.map +1 -1
  61. package/dist/transactions/transactionBuilder/encryptPayload.d.ts.map +1 -1
  62. package/dist/transactions/transactionBuilder/encryptPayload.js +37 -38
  63. package/dist/transactions/transactionBuilder/encryptPayload.js.map +1 -1
  64. package/dist/transactions/types.d.ts +17 -13
  65. package/dist/transactions/types.d.ts.map +1 -1
  66. package/dist/utils/helpers.d.ts +16 -0
  67. package/dist/utils/helpers.d.ts.map +1 -1
  68. package/dist/utils/helpers.js +29 -0
  69. package/dist/utils/helpers.js.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.js +1 -1
  72. package/package.json +3 -2
  73. package/src/account/AbstractKeylessAccount.ts +3 -0
  74. package/src/account/EphemeralKeyPair.ts +35 -8
  75. package/src/bcs/deserializer.ts +16 -0
  76. package/src/cli/index.ts +1 -0
  77. package/src/cli/localNode.ts +7 -0
  78. package/src/cli/move.ts +9 -0
  79. package/src/cli/spawnArgs.ts +55 -0
  80. package/src/core/crypto/ed25519.ts +132 -15
  81. package/src/core/crypto/keyless.ts +22 -3
  82. package/src/core/crypto/poseidon.ts +5 -5
  83. package/src/core/crypto/secp256k1.ts +141 -13
  84. package/src/core/crypto/secp256r1.ts +164 -11
  85. package/src/core/crypto/utils.ts +28 -1
  86. package/src/errors/index.ts +37 -0
  87. package/src/internal/account.ts +73 -17
  88. package/src/internal/keyless.ts +88 -2
  89. package/src/internal/transaction.ts +22 -3
  90. package/src/transactions/transactionBuilder/encryptPayload.ts +51 -45
  91. package/src/transactions/types.ts +17 -13
  92. package/src/utils/helpers.ts +33 -0
  93. package/src/version.ts +1 -1
@@ -3,6 +3,7 @@ import kill from "tree-kill";
3
3
  import { platform } from "node:os";
4
4
 
5
5
  import { sleep } from "../utils/helpers.js";
6
+ import { assertSafeCliArgs } from "./spawnArgs.js";
6
7
 
7
8
  /**
8
9
  * Represents a local node for running a localnet environment.
@@ -86,6 +87,12 @@ export class LocalNode {
86
87
  * @category CLI
87
88
  */
88
89
  start(): void {
90
+ // Reject shell-metacharacter-bearing extras up front. On Windows we have
91
+ // to spawn with `shell: true` (npx is a .cmd shim and Node refuses to
92
+ // spawn .cmd/.bat without the shell since CVE-2024-27980), so unsafe
93
+ // characters in extraArgs would otherwise be interpreted by cmd.exe.
94
+ assertSafeCliArgs(this.extraArgs);
95
+
89
96
  const cliCommand = "npx";
90
97
  const cliArgs = [
91
98
  "aptos",
package/src/cli/move.ts CHANGED
@@ -3,6 +3,7 @@ import { platform } from "node:os";
3
3
 
4
4
  import { AccountAddress } from "../core/index.js";
5
5
  import { Network } from "../utils/index.js";
6
+ import { assertSafeCliArgs } from "./spawnArgs.js";
6
7
 
7
8
  /**
8
9
  * Class representing a Move package management utility for the Aptos blockchain.
@@ -355,6 +356,14 @@ export class Move {
355
356
  */
356
357
 
357
358
  private async runCommand(args: Array<string>, showStdout: boolean = true): Promise<{ result?: any; output: string }> {
359
+ // Reject shell-metacharacter-bearing args up front. On Windows we have to
360
+ // spawn with `shell: true` (npx is a .cmd shim and Node refuses to spawn
361
+ // .cmd/.bat without the shell since CVE-2024-27980), so unsafe characters
362
+ // in args would otherwise be interpreted by cmd.exe. We validate on all
363
+ // platforms for consistency — extraArguments shouldn't contain shell
364
+ // metacharacters regardless of OS.
365
+ assertSafeCliArgs(args);
366
+
358
367
  return new Promise((resolve, reject) => {
359
368
  const isWindows = platform() === "win32";
360
369
  const spawnOptions = isWindows ? { shell: true } : undefined;
@@ -0,0 +1,55 @@
1
+ // Copyright © Aptos Foundation
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Shell metacharacters that enable command injection when passed through
6
+ * `spawn(..., { shell: true })`. We have to keep `shell: true` on Windows
7
+ * because Node.js 18.20.2+ / 20.12.2+ refuses to spawn `.cmd`/`.bat` shims
8
+ * (like `npx.cmd`) without it (CVE-2024-27980 mitigation), so the next-best
9
+ * mitigation is to reject argument strings that contain characters the shell
10
+ * would interpret.
11
+ *
12
+ * - cmd.exe metacharacters: `& | < > ^ ( ) " ' % !` plus newlines
13
+ * (`%` triggers environment-variable expansion like `%USERPROFILE%`;
14
+ * `!` triggers delayed expansion).
15
+ * - /bin/sh metacharacters: `& | ; < > ` $ ( ) "` `'` plus newlines.
16
+ *
17
+ * Backslash is intentionally NOT in the blocklist even though it's a
18
+ * /bin/sh escape character — Windows paths (`C:\Program Files\...`) and
19
+ * Windows-style flag values rely on it, and disallowing it would break
20
+ * legitimate `extraArgs` usage on every Move/LocalNode call from a Windows
21
+ * developer machine. The trade-off: on POSIX a caller passing `"a\b"` will
22
+ * have the backslash interpreted by `/bin/sh`, but our spawn call uses
23
+ * `shell: false` on POSIX anyway, so this doesn't materially matter.
24
+ *
25
+ * Common, legitimate CLI argument characters (letters, digits, `-`, `_`,
26
+ * `=`, `.`, `,`, `:`, `/`, `\`, space) are unaffected.
27
+ */
28
+ const UNSAFE_SHELL_CHARS = /[&|;<>`$()"'\n\r^!*?%]/;
29
+
30
+ /**
31
+ * Validates that a CLI argument does not contain shell metacharacters that
32
+ * could be interpreted by the shell when passed via `spawn(..., { shell: true })`.
33
+ *
34
+ * @throws Error if `arg` contains any unsafe shell character.
35
+ */
36
+ export function assertSafeCliArg(arg: string): void {
37
+ if (typeof arg !== "string") {
38
+ throw new Error(`CLI argument must be a string, received ${typeof arg}`);
39
+ }
40
+ if (UNSAFE_SHELL_CHARS.test(arg)) {
41
+ throw new Error(
42
+ `CLI argument contains characters that could be interpreted by the shell: ${JSON.stringify(arg)}. ` +
43
+ "Remove shell metacharacters (& | ; < > \" ' ` $ ( ) ^ ! * ? % newlines).",
44
+ );
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Validates every element of an args array. See {@link assertSafeCliArg}.
50
+ */
51
+ export function assertSafeCliArgs(args: ReadonlyArray<string>): void {
52
+ for (const arg of args) {
53
+ assertSafeCliArg(arg);
54
+ }
55
+ }
@@ -11,6 +11,7 @@ import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, mnemonicToSee
11
11
  import { PrivateKey } from "./privateKey.js";
12
12
  import { AccountPublicKey, PublicKey, VerifySignatureArgs, VerifySignatureAsyncArgs } from "./publicKey.js";
13
13
  import { Signature } from "./signature.js";
14
+ import { TEXT_ENCODER } from "../../utils/const.js";
14
15
  import { convertSigningMessage } from "./utils.js";
15
16
 
16
17
  /**
@@ -98,26 +99,63 @@ export class Ed25519PublicKey extends AccountPublicKey {
98
99
  // region AccountPublicKey
99
100
 
100
101
  /**
101
- * Verifies a signed message using a public key.
102
+ * Verifies a signature against the exact bytes of `message`. This is the
103
+ * unambiguous form — the input is interpreted as raw bytes regardless of
104
+ * what they encode. Pair with {@link Ed25519PrivateKey.signBytes}.
105
+ *
106
+ * Performs an Ed25519 malleability check (rejects non-canonical S values)
107
+ * before delegating to the underlying curve verifier.
102
108
  *
103
109
  * @param args - The arguments for verification.
104
- * @param args.message - A signed message as a Hex string or Uint8Array.
105
- * @param args.signature - The signature of the message.
110
+ * @param args.message - The exact bytes that were signed.
111
+ * @param args.signature - The signature to verify.
106
112
  * @group Implementation
107
113
  * @category Serialization
108
114
  */
109
- verifySignature(args: VerifySignatureArgs): boolean {
115
+ verifyBytes(args: { message: Uint8Array; signature: Signature }): boolean {
110
116
  const { message, signature } = args;
111
- // Verify malleability
112
117
  if (!isCanonicalEd25519Signature(signature)) {
113
118
  return false;
114
119
  }
120
+ return ed25519.verify(signature.toUint8Array(), message, this.key.toUint8Array());
121
+ }
122
+
123
+ /**
124
+ * Verifies a signature against the UTF-8 encoding of `message`. The input
125
+ * is always treated as text — there is no hex/text heuristic. Pair with
126
+ * {@link Ed25519PrivateKey.signText}.
127
+ *
128
+ * @param args - The arguments for verification.
129
+ * @param args.message - The text that was signed.
130
+ * @param args.signature - The signature to verify.
131
+ * @group Implementation
132
+ * @category Serialization
133
+ */
134
+ verifyText(args: { message: string; signature: Signature }): boolean {
135
+ return this.verifyBytes({ message: TEXT_ENCODER.encode(args.message), signature: args.signature });
136
+ }
115
137
 
138
+ /**
139
+ * Verifies a signed message using a public key.
140
+ *
141
+ * @deprecated The polymorphic `message: HexInput` input is ambiguous — a
142
+ * bare even-length string of hex characters (e.g., `"cafe"`) is
143
+ * interpreted as the 2 bytes `[0xCA, 0xFE]`, not as 4 UTF-8 text bytes.
144
+ * Use {@link verifyBytes} for `Uint8Array` input or {@link verifyText}
145
+ * for `string` input; both are unambiguous. See
146
+ * {@link convertSigningMessage} for the full legacy rule.
147
+ *
148
+ * @param args - The arguments for verification.
149
+ * @param args.message - A signed message as a Hex string or Uint8Array.
150
+ * @param args.signature - The signature of the message.
151
+ * @group Implementation
152
+ * @category Serialization
153
+ */
154
+ verifySignature(args: VerifySignatureArgs): boolean {
155
+ const { message, signature } = args;
116
156
  const messageToVerify = convertSigningMessage(message);
117
157
  const messageBytes = Hex.fromHexInput(messageToVerify).toUint8Array();
118
- const signatureBytes = signature.toUint8Array();
119
- const publicKeyBytes = this.key.toUint8Array();
120
- return ed25519.verify(signatureBytes, messageBytes, publicKeyBytes);
158
+ return this.verifyBytes({ message: messageBytes, signature });
121
159
  }
122
160
 
123
161
  /**
@@ -362,11 +400,40 @@ export class Ed25519PrivateKey extends Serializable implements PrivateKey {
362
400
  }
363
401
 
364
402
  /**
365
- * Clears the private key from memory by overwriting it with random bytes.
366
- * After calling this method, the private key can no longer be used for signing or deriving public keys.
403
+ * Overwrites the underlying private-key byte buffer with random bytes and
404
+ * then zeros. After calling this method the key can no longer sign or
405
+ * derive a public key.
406
+ *
407
+ * SECURITY: This is a best-effort window-narrowing tool, NOT a true
408
+ * zeroization guarantee. In JavaScript, four classes of copies cannot be
409
+ * reached from user code and so survive `clear()`:
367
410
  *
368
- * Note: Due to JavaScript's memory management, this cannot guarantee complete removal of
369
- * sensitive data from memory, but it significantly reduces the window of exposure.
411
+ * 1. **JS string copies.** Any value previously produced by `toString()`,
412
+ * `toHexString()`, or `bcsToHex().toString()` is an immutable string
413
+ * in the heap. The language provides no API to overwrite string
414
+ * memory; it is reclaimed only when GC collects it.
415
+ * 2. **noble-curves internals.** The sign path inside `@noble/curves`
416
+ * expands the private key into scalar `BigInt` field elements, which
417
+ * are also immutable in V8/JSC/Hermes. Even if noble explicitly zeroed
418
+ * its own byte copies after use, the `BigInt` intermediates persist.
419
+ * 3. **JIT register / stack residue.** The engine may have held key
420
+ * bytes in CPU registers or on the engine stack during a sign call.
421
+ * There is no JS-visible way to scrub those.
422
+ * 4. **GC-relocated copies.** Generational GCs (V8, JSC) copy live
423
+ * objects between heap regions during minor/major collections. The
424
+ * `Uint8Array` we zeroed may have stale copies sitting in survivor
425
+ * space until the next cycle reclaims them.
426
+ *
427
+ * This method zeros the SDK's own `Uint8Array` (the most reachable
428
+ * copy), but downstream consumers should treat it as a hardening signal,
429
+ * not a guarantee. If you need real key-material hygiene, prefer
430
+ * non-extractable `crypto.subtle` keys (where the underlying algorithm
431
+ * is supported by the host runtime), a WASM-backed crypto library, or
432
+ * hardware-backed keys (passkeys / secure enclave / HSM).
433
+ *
434
+ * To minimize the size of the unreachable-copy set, avoid calling
435
+ * `toString()` / `toHexString()` on private keys at all in long-lived
436
+ * processes — the byte form is what gets cleared.
370
437
  *
371
438
  * @group Implementation
372
439
  * @category Serialization
@@ -412,10 +479,49 @@ export class Ed25519PrivateKey extends Serializable implements PrivateKey {
412
479
  return new Ed25519PublicKey(bytes);
413
480
  }
414
481
 
482
+ /**
483
+ * Sign exactly the bytes of `message`. The input is interpreted as raw
484
+ * bytes regardless of what they encode. Pair with
485
+ * {@link Ed25519PublicKey.verifyBytes}.
486
+ *
487
+ * @param message - The exact bytes to sign.
488
+ * @returns A digital signature for the provided bytes.
489
+ * @throws Error if the private key has been cleared from memory.
490
+ * @group Implementation
491
+ * @category Serialization
492
+ */
493
+ signBytes(message: Uint8Array): Ed25519Signature {
494
+ this.ensureNotCleared();
495
+ const signatureBytes = ed25519.sign(message, this.signingKey.toUint8Array());
496
+ return new Ed25519Signature(signatureBytes);
497
+ }
498
+
499
+ /**
500
+ * Sign the UTF-8 encoding of `message`. The input is always treated as
501
+ * text — there is no hex/text heuristic. Pair with
502
+ * {@link Ed25519PublicKey.verifyText}.
503
+ *
504
+ * @param message - The text to sign.
505
+ * @returns A digital signature for the UTF-8 bytes of the provided text.
506
+ * @throws Error if the private key has been cleared from memory.
507
+ * @group Implementation
508
+ * @category Serialization
509
+ */
510
+ signText(message: string): Ed25519Signature {
511
+ return this.signBytes(TEXT_ENCODER.encode(message));
512
+ }
513
+
415
514
  /**
416
515
  * Sign the given message with the private key.
417
516
  * This function generates a digital signature for the specified message, ensuring its authenticity and integrity.
418
517
  *
518
+ * @deprecated The polymorphic `message: HexInput` input is ambiguous — a
519
+ * bare even-length string of hex characters (e.g., `"cafe"`) is signed
520
+ * as the 2 bytes `[0xCA, 0xFE]`, not as 4 UTF-8 text bytes. Use
521
+ * {@link signBytes} for `Uint8Array` input or {@link signText} for
522
+ * `string` input; both are unambiguous. See
523
+ * {@link convertSigningMessage} for the full legacy rule.
524
+ *
419
525
  * @param message - A message as a string or Uint8Array in HexInput format.
420
526
  * @returns A digital signature for the provided message.
421
527
  * @throws Error if the private key has been cleared from memory.
@@ -423,11 +529,9 @@ export class Ed25519PrivateKey extends Serializable implements PrivateKey {
423
529
  * @category Serialization
424
530
  */
425
531
  sign(message: HexInput): Ed25519Signature {
426
- this.ensureNotCleared();
427
532
  const messageToSign = convertSigningMessage(message);
428
533
  const messageBytes = Hex.fromHexInput(messageToSign).toUint8Array();
429
- const signatureBytes = ed25519.sign(messageBytes, this.signingKey.toUint8Array());
430
- return new Ed25519Signature(signatureBytes);
534
+ return this.signBytes(messageBytes);
431
535
  }
432
536
 
433
537
  /**
@@ -446,6 +550,13 @@ export class Ed25519PrivateKey extends Serializable implements PrivateKey {
446
550
  /**
447
551
  * Get the private key as a hex string with the 0x prefix.
448
552
  *
553
+ * SECURITY: This produces an immutable JS string containing the key
554
+ * material in hex. Strings cannot be zeroed by `clear()` (see the
555
+ * `clear()` JSDoc for the four classes of unreachable copies). Avoid
556
+ * calling this method on long-lived `Ed25519PrivateKey` instances in
557
+ * processes where memory hygiene matters; prefer `toUint8Array()`,
558
+ * which returns a clearable `Uint8Array`.
559
+ *
449
560
  * @returns string representation of the private key.
450
561
  * @throws Error if the private key has been cleared from memory.
451
562
  * @group Implementation
@@ -459,6 +570,9 @@ export class Ed25519PrivateKey extends Serializable implements PrivateKey {
459
570
  /**
460
571
  * Get the private key as a hex string with the 0x prefix.
461
572
  *
573
+ * SECURITY: Same caveat as `toString()` — the returned string is an
574
+ * immutable JS heap allocation that `clear()` cannot zero.
575
+ *
462
576
  * @returns string representation of the private key.
463
577
  * @throws Error if the private key has been cleared from memory.
464
578
  */
@@ -472,6 +586,9 @@ export class Ed25519PrivateKey extends Serializable implements PrivateKey {
472
586
  *
473
587
  * [Read about AIP-80](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md)
474
588
  *
589
+ * SECURITY: Same caveat as `toString()` — produces an immutable JS string
590
+ * containing the key material; cannot be zeroed by `clear()`.
591
+ *
475
592
  * @returns AIP-80 compliant string representation of the private key.
476
593
  * @throws Error if the private key has been cleared from memory.
477
594
  */
@@ -28,6 +28,7 @@ import {
28
28
  PatchedJWKsResponse,
29
29
  } from "../../types/keyless.js";
30
30
  import { AptosConfig } from "../../api/aptosConfig.js";
31
+ import { u64ToNumberSafe } from "../../utils/helpers.js";
31
32
  import { getAptosFullNode } from "../../client/index.js";
32
33
  import { memoizeAsync } from "../../utils/memoize.js";
33
34
  import { AccountAddress, AccountAddressInput } from "../accountAddress.js";
@@ -295,6 +296,14 @@ export class KeylessPublicKey extends AccountPublicKey {
295
296
  * Creates a KeylessPublicKey instance from a JWT and a pepper value.
296
297
  * This function is useful for generating a public key that can be used for authentication based on the provided JWT claims and pepper.
297
298
  *
299
+ * SECURITY: `jwtDecode` is a decode-only library — it does NOT verify the
300
+ * JWT signature. The cryptographic binding between the JWT and the user's
301
+ * identity is enforced on-chain by the keyless verifier (which validates
302
+ * the JWT signature against the JWK set published on-chain). Callers MUST
303
+ * therefore obtain `jwt` directly from a trusted IdP redirect/OAuth flow;
304
+ * do not accept arbitrary user-supplied JWT strings here, since a tampered
305
+ * JWT will derive a different account address than the chain expects.
306
+ *
298
307
  * @param args - The arguments for creating the KeylessPublicKey.
299
308
  * @param args.jwt - The JSON Web Token to decode.
300
309
  * @param args.pepper - The pepper value used in the key creation process.
@@ -305,6 +314,7 @@ export class KeylessPublicKey extends AccountPublicKey {
305
314
  */
306
315
  static fromJwtAndPepper(args: { jwt: string; pepper: HexInput; uidKey?: string }): KeylessPublicKey {
307
316
  const { jwt, pepper, uidKey = "sub" } = args;
317
+ // SECURITY: signature is not verified here — see method-level JSDoc.
308
318
  const jwtPayload = jwtDecode<JwtPayload & { [key: string]: string }>(jwt);
309
319
  if (typeof jwtPayload.iss !== "string") {
310
320
  throw new Error("iss was not found");
@@ -652,7 +662,7 @@ export class KeylessSignature extends Signature {
652
662
  const ephemeralSignature = EphemeralSignature.deserialize(deserializer);
653
663
  return new KeylessSignature({
654
664
  jwtHeader,
655
- expiryDateSecs: Number(expiryDateSecs),
665
+ expiryDateSecs: u64ToNumberSafe(expiryDateSecs, "KeylessSignature.expiryDateSecs"),
656
666
  ephemeralCertificate,
657
667
  ephemeralPublicKey,
658
668
  ephemeralSignature,
@@ -1116,7 +1126,7 @@ export class ZeroKnowledgeSig extends Signature {
1116
1126
 
1117
1127
  static deserialize(deserializer: Deserializer): ZeroKnowledgeSig {
1118
1128
  const proof = ZkProof.deserialize(deserializer);
1119
- const expHorizonSecs = Number(deserializer.deserializeU64());
1129
+ const expHorizonSecs = u64ToNumberSafe(deserializer.deserializeU64(), "ZeroKnowledgeSig.expHorizonSecs");
1120
1130
  const extraField = deserializer.deserializeOption("string");
1121
1131
  const overrideAudVal = deserializer.deserializeOption("string");
1122
1132
  const trainingWheelsSignature = deserializer.deserializeOption(EphemeralSignature);
@@ -1228,7 +1238,9 @@ export class KeylessConfiguration {
1228
1238
  gammaAbcG1: res.gamma_abc_g1,
1229
1239
  gammaG2: res.gamma_g2,
1230
1240
  }),
1231
- maxExpHorizonSecs: Number(config.max_exp_horizon_secs),
1241
+ // Chain config returns u64 as a decimal string; widen → safe-narrow so
1242
+ // a malformed/exotic value throws rather than silently truncates.
1243
+ maxExpHorizonSecs: u64ToNumberSafe(BigInt(config.max_exp_horizon_secs), "KeylessConfiguration.maxExpHorizonSecs"),
1232
1244
  trainingWheelsPubkey: config.training_wheels_pubkey.vec[0],
1233
1245
  maxExtraFieldBytes: config.max_extra_field_bytes,
1234
1246
  maxJwtHeaderB64Bytes: config.max_jwt_header_b64_bytes,
@@ -1457,6 +1469,12 @@ export async function getKeylessConfig(args: {
1457
1469
  /**
1458
1470
  * Parses a JWT and returns the 'iss', 'aud', and 'uid' values.
1459
1471
  *
1472
+ * SECURITY: This function decodes claims without verifying the JWT signature.
1473
+ * The keyless on-chain verifier is the authority that binds a JWT to its IdP;
1474
+ * the SDK only uses these claims to derive the keyless account address and
1475
+ * package the JWT for the prover service. Callers must source `jwt` from a
1476
+ * trusted IdP redirect flow.
1477
+ *
1460
1478
  * @param args - The arguments for parsing the JWT.
1461
1479
  * @param args.jwt - The JWT to parse.
1462
1480
  * @param args.uidKey - The key to use for the 'uid' value; defaults to 'sub'.
@@ -1470,6 +1488,7 @@ export function getIssAudAndUidVal(args: { jwt: string; uidKey?: string }): {
1470
1488
  const { jwt, uidKey = "sub" } = args;
1471
1489
  let jwtPayload: JwtPayload & { [key: string]: string };
1472
1490
  try {
1491
+ // SECURITY: signature is not verified here — see function-level JSDoc.
1473
1492
  jwtPayload = jwtDecode<JwtPayload & { [key: string]: string }>(jwt);
1474
1493
  } catch {
1475
1494
  throw KeylessError.fromErrorType({
@@ -68,7 +68,7 @@ export function hashStrToField(str: string, maxSizeBytes: number): bigint {
68
68
  */
69
69
  function hashBytesWithLen(bytes: Uint8Array, maxSizeBytes: number): bigint {
70
70
  if (bytes.length > maxSizeBytes) {
71
- throw new Error(`Inputted bytes of length ${bytes} is longer than ${maxSizeBytes}`);
71
+ throw new Error(`Input bytes of length ${bytes.length} is longer than ${maxSizeBytes}`);
72
72
  }
73
73
  const packed = padAndPackBytesWithLen(bytes, maxSizeBytes);
74
74
  return poseidonHash(packed);
@@ -86,7 +86,7 @@ function hashBytesWithLen(bytes: Uint8Array, maxSizeBytes: number): bigint {
86
86
  */
87
87
  function padAndPackBytesNoLen(bytes: Uint8Array, maxSizeBytes: number): bigint[] {
88
88
  if (bytes.length > maxSizeBytes) {
89
- throw new Error(`Input bytes of length ${bytes} is longer than ${maxSizeBytes}`);
89
+ throw new Error(`Input bytes of length ${bytes.length} is longer than ${maxSizeBytes}`);
90
90
  }
91
91
  const paddedStrBytes = padUint8ArrayWithZeros(bytes, maxSizeBytes);
92
92
  return packBytes(paddedStrBytes);
@@ -106,7 +106,7 @@ function padAndPackBytesNoLen(bytes: Uint8Array, maxSizeBytes: number): bigint[]
106
106
  */
107
107
  export function padAndPackBytesWithLen(bytes: Uint8Array, maxSizeBytes: number): bigint[] {
108
108
  if (bytes.length > maxSizeBytes) {
109
- throw new Error(`Input bytes of length ${bytes} is longer than ${maxSizeBytes}`);
109
+ throw new Error(`Input bytes of length ${bytes.length} is longer than ${maxSizeBytes}`);
110
110
  }
111
111
  return padAndPackBytesNoLen(bytes, maxSizeBytes).concat([BigInt(bytes.length)]);
112
112
  }
@@ -221,9 +221,9 @@ function padUint8ArrayWithZeros(inputArray: Uint8Array, paddedSize: number): Uin
221
221
  * @category Serialization
222
222
  */
223
223
  export function poseidonHash(inputs: (number | bigint | string)[]): bigint {
224
- if (inputs.length > numInputsToPoseidonFunc.length) {
224
+ if (inputs.length === 0 || inputs.length > numInputsToPoseidonFunc.length) {
225
225
  throw new Error(
226
- `Unable to hash input of length ${inputs.length}. Max input length is ${numInputsToPoseidonFunc.length}`,
226
+ `poseidonHash requires between 1 and ${numInputsToPoseidonFunc.length} inputs, got ${inputs.length}`,
227
227
  );
228
228
  }
229
229
  return numInputsToPoseidonFunc[inputs.length - 1](inputs);
@@ -12,6 +12,7 @@ import { PrivateKey } from "./privateKey.js";
12
12
  import { PublicKey } from "./publicKey.js";
13
13
  import { Signature } from "./signature.js";
14
14
  import { convertSigningMessage } from "./utils.js";
15
+ import { TEXT_ENCODER } from "../../utils/const.js";
15
16
  import { AptosConfig } from "../../api/aptosConfig.js";
16
17
 
17
18
  /**
@@ -62,10 +63,54 @@ export class Secp256k1PublicKey extends PublicKey {
62
63
  }
63
64
 
64
65
  // region PublicKey
66
+ /**
67
+ * Verifies a signature against the exact bytes of `message`. This is the
68
+ * unambiguous form — the input is interpreted as raw bytes regardless of
69
+ * what they encode. Pair with {@link Secp256k1PrivateKey.signBytes}.
70
+ *
71
+ * The message is SHA3-256 hashed before verification (matching the
72
+ * Aptos-side Secp256k1 signing convention), and the signature is required
73
+ * to be in canonical low-S form for malleability resistance.
74
+ *
75
+ * @param args - The arguments for verification.
76
+ * @param args.message - The exact bytes that were signed.
77
+ * @param args.signature - The signature to verify.
78
+ * @group Implementation
79
+ * @category Serialization
80
+ */
81
+ verifyBytes(args: { message: Uint8Array; signature: Secp256k1Signature }): boolean {
82
+ const { message, signature } = args;
83
+ const messageSha3Bytes = sha3_256(message);
84
+ return secp256k1.verify(signature.toUint8Array(), messageSha3Bytes, this.key.toUint8Array(), {
85
+ lowS: true,
86
+ prehash: false,
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Verifies a signature against the UTF-8 encoding of `message`. The input
92
+ * is always treated as text — there is no hex/text heuristic. Pair with
93
+ * {@link Secp256k1PrivateKey.signText}.
94
+ *
95
+ * @param args - The arguments for verification.
96
+ * @param args.message - The text that was signed.
97
+ * @param args.signature - The signature to verify.
98
+ * @group Implementation
99
+ * @category Serialization
100
+ */
101
+ verifyText(args: { message: string; signature: Secp256k1Signature }): boolean {
102
+ return this.verifyBytes({ message: TEXT_ENCODER.encode(args.message), signature: args.signature });
103
+ }
104
+
65
105
  /**
66
106
  * Verifies a Secp256k1 signature against the public key.
67
107
  *
68
- * This function checks the validity of a signature for a given message, ensuring that the signature is canonical as a malleability check.
108
+ * @deprecated The polymorphic `message: HexInput` input is ambiguous a
109
+ * bare even-length string of hex characters (e.g., `"cafe"`) is verified
110
+ * against the 2 bytes `[0xCA, 0xFE]`, not 4 UTF-8 text bytes. Use
111
+ * {@link verifyBytes} for `Uint8Array` input or {@link verifyText} for
112
+ * `string` input; both are unambiguous. See
113
+ * {@link convertSigningMessage} for the full legacy rule.
69
114
  *
70
115
  * @param args - The arguments for verifying the signature.
71
116
  * @param args.message - The message that was signed.
@@ -77,9 +122,7 @@ export class Secp256k1PublicKey extends PublicKey {
77
122
  const { message, signature } = args;
78
123
  const messageToVerify = convertSigningMessage(message);
79
124
  const messageBytes = Hex.fromHexInput(messageToVerify).toUint8Array();
80
- const messageSha3Bytes = sha3_256(messageBytes);
81
- const signatureBytes = signature.toUint8Array();
82
- return secp256k1.verify(signatureBytes, messageSha3Bytes, this.key.toUint8Array(), { lowS: true, prehash: false });
125
+ return this.verifyBytes({ message: messageBytes, signature });
83
126
  }
84
127
 
85
128
  /**
@@ -308,11 +351,42 @@ export class Secp256k1PrivateKey extends Serializable implements PrivateKey {
308
351
  }
309
352
 
310
353
  /**
311
- * Clears the private key from memory by overwriting it with random bytes.
312
- * After calling this method, the private key can no longer be used for signing or deriving public keys.
354
+ * Overwrites the underlying private-key byte buffer with random bytes and
355
+ * then zeros. After calling this method the key can no longer sign or
356
+ * derive a public key.
357
+ *
358
+ * SECURITY: This is a best-effort window-narrowing tool, NOT a true
359
+ * zeroization guarantee. In JavaScript, four classes of copies cannot be
360
+ * reached from user code and so survive `clear()`:
361
+ *
362
+ * 1. **JS string copies.** Any value previously produced by `toString()`,
363
+ * `toHexString()`, or `bcsToHex().toString()` is an immutable string
364
+ * in the heap. The language provides no API to overwrite string
365
+ * memory; it is reclaimed only when GC collects it.
366
+ * 2. **noble-curves internals.** The sign path inside `@noble/curves`
367
+ * expands the private key into scalar `BigInt` field elements, which
368
+ * are also immutable. Even if noble explicitly zeroed its own byte
369
+ * copies after use, the `BigInt` intermediates persist.
370
+ * 3. **JIT register / stack residue.** The engine may have held key
371
+ * bytes in CPU registers or on the engine stack during a sign call.
372
+ * There is no JS-visible way to scrub those.
373
+ * 4. **GC-relocated copies.** Generational GCs (V8, JSC) copy live
374
+ * objects between heap regions during minor/major collections. The
375
+ * `Uint8Array` we zeroed may have stale copies sitting in survivor
376
+ * space until the next cycle reclaims them.
313
377
  *
314
- * Note: Due to JavaScript's memory management, this cannot guarantee complete removal of
315
- * sensitive data from memory, but it significantly reduces the window of exposure.
378
+ * This method zeros the SDK's own `Uint8Array` (the most reachable
379
+ * copy), but downstream consumers should treat it as a hardening signal,
380
+ * not a guarantee. If you need real key-material hygiene, prefer a
381
+ * WASM-backed secp256k1 implementation that exposes explicit
382
+ * `memzero`, or hardware-backed keys (HSM / TPM / secure enclave).
383
+ * Note that `crypto.subtle` does NOT support secp256k1 on any major
384
+ * runtime, so non-extractable WebCrypto keys are not available for
385
+ * this curve.
386
+ *
387
+ * To minimize the size of the unreachable-copy set, avoid calling
388
+ * `toString()` / `toHexString()` on private keys at all in long-lived
389
+ * processes — the byte form is what gets cleared.
316
390
  *
317
391
  * @group Implementation
318
392
  * @category Serialization
@@ -344,10 +418,54 @@ export class Secp256k1PrivateKey extends Serializable implements PrivateKey {
344
418
  return this.cleared;
345
419
  }
346
420
 
421
+ /**
422
+ * Sign exactly the bytes of `message`. The input is interpreted as raw
423
+ * bytes regardless of what they encode. Pair with
424
+ * {@link Secp256k1PublicKey.verifyBytes}.
425
+ *
426
+ * The message is SHA3-256 hashed before signing (matching the Aptos-side
427
+ * Secp256k1 signing convention), and the produced signature is in
428
+ * canonical low-S form for malleability resistance.
429
+ *
430
+ * @param message - The exact bytes to sign.
431
+ * @returns The generated signature for the provided bytes.
432
+ * @throws Error if the private key has been cleared from memory.
433
+ * @group Implementation
434
+ * @category Serialization
435
+ */
436
+ signBytes(message: Uint8Array): Secp256k1Signature {
437
+ this.ensureNotCleared();
438
+ const messageHashBytes = sha3_256(message);
439
+ const signature = secp256k1.sign(messageHashBytes, this.key.toUint8Array(), { lowS: true, prehash: false });
440
+ return new Secp256k1Signature(signature);
441
+ }
442
+
443
+ /**
444
+ * Sign the UTF-8 encoding of `message`. The input is always treated as
445
+ * text — there is no hex/text heuristic. Pair with
446
+ * {@link Secp256k1PublicKey.verifyText}.
447
+ *
448
+ * @param message - The text to sign.
449
+ * @returns The generated signature for the UTF-8 bytes of the provided text.
450
+ * @throws Error if the private key has been cleared from memory.
451
+ * @group Implementation
452
+ * @category Serialization
453
+ */
454
+ signText(message: string): Secp256k1Signature {
455
+ return this.signBytes(TEXT_ENCODER.encode(message));
456
+ }
457
+
347
458
  /**
348
459
  * Sign the given message with the private key.
349
460
  * This function generates a cryptographic signature for the provided message, ensuring the signature is canonical and non-malleable.
350
461
  *
462
+ * @deprecated The polymorphic `message: HexInput` input is ambiguous — a
463
+ * bare even-length string of hex characters (e.g., `"cafe"`) is signed
464
+ * as the 2 bytes `[0xCA, 0xFE]`, not 4 UTF-8 text bytes. Use
465
+ * {@link signBytes} for `Uint8Array` input or {@link signText} for
466
+ * `string` input; both are unambiguous. See
467
+ * {@link convertSigningMessage} for the full legacy rule.
468
+ *
351
469
  * @param message - A message in HexInput format to be signed.
352
470
  * @returns Signature - The generated signature for the provided message.
353
471
  * @throws Error if the private key has been cleared from memory.
@@ -355,12 +473,9 @@ export class Secp256k1PrivateKey extends Serializable implements PrivateKey {
355
473
  * @category Serialization
356
474
  */
357
475
  sign(message: HexInput): Secp256k1Signature {
358
- this.ensureNotCleared();
359
476
  const messageToSign = convertSigningMessage(message);
360
- const messageBytes = Hex.fromHexInput(messageToSign);
361
- const messageHashBytes = sha3_256(messageBytes.toUint8Array());
362
- const signature = secp256k1.sign(messageHashBytes, this.key.toUint8Array(), { lowS: true, prehash: false });
363
- return new Secp256k1Signature(signature);
477
+ const messageBytes = Hex.fromHexInput(messageToSign).toUint8Array();
478
+ return this.signBytes(messageBytes);
364
479
  }
365
480
 
366
481
  /**
@@ -393,6 +508,13 @@ export class Secp256k1PrivateKey extends Serializable implements PrivateKey {
393
508
  /**
394
509
  * Get the private key as a string representation.
395
510
  *
511
+ * SECURITY: This produces an immutable JS string containing the key
512
+ * material in hex. Strings cannot be zeroed by `clear()` (see the
513
+ * `clear()` JSDoc for the four classes of unreachable copies). Avoid
514
+ * calling this method on long-lived `Secp256k1PrivateKey` instances in
515
+ * processes where memory hygiene matters; prefer `toUint8Array()`,
516
+ * which returns a clearable `Uint8Array`.
517
+ *
396
518
  * @returns string representation of the private key
397
519
  * @throws Error if the private key has been cleared from memory.
398
520
  * @group Implementation
@@ -406,6 +528,9 @@ export class Secp256k1PrivateKey extends Serializable implements PrivateKey {
406
528
  /**
407
529
  * Get the private key as a hex string with the 0x prefix.
408
530
  *
531
+ * SECURITY: Same caveat as `toString()` — the returned string is an
532
+ * immutable JS heap allocation that `clear()` cannot zero.
533
+ *
409
534
  * @returns string representation of the private key.
410
535
  * @throws Error if the private key has been cleared from memory.
411
536
  */
@@ -419,6 +544,9 @@ export class Secp256k1PrivateKey extends Serializable implements PrivateKey {
419
544
  *
420
545
  * [Read about AIP-80](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md)
421
546
  *
547
+ * SECURITY: Same caveat as `toString()` — produces an immutable JS string
548
+ * containing the key material; cannot be zeroed by `clear()`.
549
+ *
422
550
  * @returns AIP-80 compliant string representation of the private key.
423
551
  * @throws Error if the private key has been cleared from memory.
424
552
  */