@volr/sdk-core 0.1.65 → 0.1.67

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/index.d.cts CHANGED
@@ -30,6 +30,25 @@ type AesGcmParams = {
30
30
  /** Additional Authenticated Data (optional) */
31
31
  aad?: Uint8Array;
32
32
  };
33
+ /**
34
+ * Generate a unique 12-byte nonce for AES-GCM
35
+ *
36
+ * Format: [4 bytes timestamp] + [8 bytes random]
37
+ *
38
+ * This hybrid approach provides:
39
+ * 1. Timestamp component: Reduces collision probability across different times
40
+ * 2. Random component: Reduces collision probability at the same time
41
+ *
42
+ * Collision analysis:
43
+ * - With purely random 96-bit nonces: 2^32 encryptions before 50% collision probability
44
+ * - With timestamp + 64-bit random: Collisions require same second + same random (2^64 per second)
45
+ *
46
+ * Note: This is belt-and-suspenders - in practice, master seeds are encrypted
47
+ * only once per enrollment, so collisions are extremely unlikely either way.
48
+ *
49
+ * @returns 12-byte Uint8Array nonce
50
+ */
51
+ declare function generateNonce(): Uint8Array;
33
52
  /**
34
53
  * AES-256-GCM encryption
35
54
  *
@@ -1262,6 +1281,14 @@ type PasskeyProviderOptions = {
1262
1281
  };
1263
1282
  /** Additional Authenticated Data for unwrapping */
1264
1283
  aad?: Uint8Array;
1284
+ /**
1285
+ * Session TTL in milliseconds.
1286
+ * - 0 (default): No caching - requires WebAuthn authentication for every signature (most secure)
1287
+ * - >0: Cache private key in memory for this duration (less secure, better UX)
1288
+ *
1289
+ * @default 0
1290
+ */
1291
+ sessionTtlMs?: number;
1265
1292
  };
1266
1293
  /**
1267
1294
  * Create passkey provider
@@ -1341,4 +1368,4 @@ type SelectSignerContext = {
1341
1368
  */
1342
1369
  declare function selectSigner(ctx: SelectSignerContext): Promise<SignerPort>;
1343
1370
 
1344
- export { type AesGcmParams, type AuthorizationTuple, type Call, ChainNotAddedError, DEFAULT_EVM_PATH, DOMAIN, type DeriveArgs, EIP1193ErrorCode, type EIP1193Provider, EIP712_DOMAIN, ERR_AAD_MISMATCH, ERR_CHAIN_MISMATCH, ERR_CRYPTO_FAIL, ERR_INVALID_PARAM, ERR_LOW_ENTROPY, ERR_NONCE_SIZE, type EvmKeypair, type ExtendedRPCClient, ExternalWalletSigner, type KeyStorageType, type Sig$1 as LegacySig, type MasterKeyProvider, type MasterSeed, type MasterSeedHandle, type MpcTransport, PasskeyP256Signer, type PasskeyProviderOptions, type PasskeyProviderPort, type PrecheckInput, type PrecheckQuote, type PrfInput, type RelayInput, type RelayMode, type RelayResult, RequestPendingError, type RpcLike, Secp256k1SoftwareSigner, type SelectSignerContext, type SessionAuth, type Sig, type SignerContext, type SignerKind, type SignerPort, TYPES, type TypedDataInput, UnauthorizedError, UnsupportedMethodError, type UploadBlobOptions, type UploadViaPresignOptions, UserRejectedError, VolrError, WalletError, type WalletProviderPort, type WrapKey, WrongNetworkError, ZERO_ADDRESS, ZERO_HASH, aesGcmDecrypt, aesGcmEncrypt, buildSignedBatchMessage, computeCallsHash, createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, evmSign, evmVerify, getAuthNonce, getRandomBytes, hkdfSha256, normalizeWalletError, sealMasterSeed, selectSigner, selectSigner$1 as selectSignerLegacy, signAuthorization, signSession, toChecksumAddress, unsealMasterSeed, uploadBlob, uploadBlobViaPresign, zeroize };
1371
+ export { type AesGcmParams, type AuthorizationTuple, type Call, ChainNotAddedError, DEFAULT_EVM_PATH, DOMAIN, type DeriveArgs, EIP1193ErrorCode, type EIP1193Provider, EIP712_DOMAIN, ERR_AAD_MISMATCH, ERR_CHAIN_MISMATCH, ERR_CRYPTO_FAIL, ERR_INVALID_PARAM, ERR_LOW_ENTROPY, ERR_NONCE_SIZE, type EvmKeypair, type ExtendedRPCClient, ExternalWalletSigner, type KeyStorageType, type Sig$1 as LegacySig, type MasterKeyProvider, type MasterSeed, type MasterSeedHandle, type MpcTransport, PasskeyP256Signer, type PasskeyProviderOptions, type PasskeyProviderPort, type PrecheckInput, type PrecheckQuote, type PrfInput, type RelayInput, type RelayMode, type RelayResult, RequestPendingError, type RpcLike, Secp256k1SoftwareSigner, type SelectSignerContext, type SessionAuth, type Sig, type SignerContext, type SignerKind, type SignerPort, TYPES, type TypedDataInput, UnauthorizedError, UnsupportedMethodError, type UploadBlobOptions, type UploadViaPresignOptions, UserRejectedError, VolrError, WalletError, type WalletProviderPort, type WrapKey, WrongNetworkError, ZERO_ADDRESS, ZERO_HASH, aesGcmDecrypt, aesGcmEncrypt, buildSignedBatchMessage, computeCallsHash, createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, evmSign, evmVerify, generateNonce, getAuthNonce, getRandomBytes, hkdfSha256, normalizeWalletError, sealMasterSeed, selectSigner, selectSigner$1 as selectSignerLegacy, signAuthorization, signSession, toChecksumAddress, unsealMasterSeed, uploadBlob, uploadBlobViaPresign, zeroize };
package/dist/index.d.ts CHANGED
@@ -30,6 +30,25 @@ type AesGcmParams = {
30
30
  /** Additional Authenticated Data (optional) */
31
31
  aad?: Uint8Array;
32
32
  };
33
+ /**
34
+ * Generate a unique 12-byte nonce for AES-GCM
35
+ *
36
+ * Format: [4 bytes timestamp] + [8 bytes random]
37
+ *
38
+ * This hybrid approach provides:
39
+ * 1. Timestamp component: Reduces collision probability across different times
40
+ * 2. Random component: Reduces collision probability at the same time
41
+ *
42
+ * Collision analysis:
43
+ * - With purely random 96-bit nonces: 2^32 encryptions before 50% collision probability
44
+ * - With timestamp + 64-bit random: Collisions require same second + same random (2^64 per second)
45
+ *
46
+ * Note: This is belt-and-suspenders - in practice, master seeds are encrypted
47
+ * only once per enrollment, so collisions are extremely unlikely either way.
48
+ *
49
+ * @returns 12-byte Uint8Array nonce
50
+ */
51
+ declare function generateNonce(): Uint8Array;
33
52
  /**
34
53
  * AES-256-GCM encryption
35
54
  *
@@ -1262,6 +1281,14 @@ type PasskeyProviderOptions = {
1262
1281
  };
1263
1282
  /** Additional Authenticated Data for unwrapping */
1264
1283
  aad?: Uint8Array;
1284
+ /**
1285
+ * Session TTL in milliseconds.
1286
+ * - 0 (default): No caching - requires WebAuthn authentication for every signature (most secure)
1287
+ * - >0: Cache private key in memory for this duration (less secure, better UX)
1288
+ *
1289
+ * @default 0
1290
+ */
1291
+ sessionTtlMs?: number;
1265
1292
  };
1266
1293
  /**
1267
1294
  * Create passkey provider
@@ -1341,4 +1368,4 @@ type SelectSignerContext = {
1341
1368
  */
1342
1369
  declare function selectSigner(ctx: SelectSignerContext): Promise<SignerPort>;
1343
1370
 
1344
- export { type AesGcmParams, type AuthorizationTuple, type Call, ChainNotAddedError, DEFAULT_EVM_PATH, DOMAIN, type DeriveArgs, EIP1193ErrorCode, type EIP1193Provider, EIP712_DOMAIN, ERR_AAD_MISMATCH, ERR_CHAIN_MISMATCH, ERR_CRYPTO_FAIL, ERR_INVALID_PARAM, ERR_LOW_ENTROPY, ERR_NONCE_SIZE, type EvmKeypair, type ExtendedRPCClient, ExternalWalletSigner, type KeyStorageType, type Sig$1 as LegacySig, type MasterKeyProvider, type MasterSeed, type MasterSeedHandle, type MpcTransport, PasskeyP256Signer, type PasskeyProviderOptions, type PasskeyProviderPort, type PrecheckInput, type PrecheckQuote, type PrfInput, type RelayInput, type RelayMode, type RelayResult, RequestPendingError, type RpcLike, Secp256k1SoftwareSigner, type SelectSignerContext, type SessionAuth, type Sig, type SignerContext, type SignerKind, type SignerPort, TYPES, type TypedDataInput, UnauthorizedError, UnsupportedMethodError, type UploadBlobOptions, type UploadViaPresignOptions, UserRejectedError, VolrError, WalletError, type WalletProviderPort, type WrapKey, WrongNetworkError, ZERO_ADDRESS, ZERO_HASH, aesGcmDecrypt, aesGcmEncrypt, buildSignedBatchMessage, computeCallsHash, createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, evmSign, evmVerify, getAuthNonce, getRandomBytes, hkdfSha256, normalizeWalletError, sealMasterSeed, selectSigner, selectSigner$1 as selectSignerLegacy, signAuthorization, signSession, toChecksumAddress, unsealMasterSeed, uploadBlob, uploadBlobViaPresign, zeroize };
1371
+ export { type AesGcmParams, type AuthorizationTuple, type Call, ChainNotAddedError, DEFAULT_EVM_PATH, DOMAIN, type DeriveArgs, EIP1193ErrorCode, type EIP1193Provider, EIP712_DOMAIN, ERR_AAD_MISMATCH, ERR_CHAIN_MISMATCH, ERR_CRYPTO_FAIL, ERR_INVALID_PARAM, ERR_LOW_ENTROPY, ERR_NONCE_SIZE, type EvmKeypair, type ExtendedRPCClient, ExternalWalletSigner, type KeyStorageType, type Sig$1 as LegacySig, type MasterKeyProvider, type MasterSeed, type MasterSeedHandle, type MpcTransport, PasskeyP256Signer, type PasskeyProviderOptions, type PasskeyProviderPort, type PrecheckInput, type PrecheckQuote, type PrfInput, type RelayInput, type RelayMode, type RelayResult, RequestPendingError, type RpcLike, Secp256k1SoftwareSigner, type SelectSignerContext, type SessionAuth, type Sig, type SignerContext, type SignerKind, type SignerPort, TYPES, type TypedDataInput, UnauthorizedError, UnsupportedMethodError, type UploadBlobOptions, type UploadViaPresignOptions, UserRejectedError, VolrError, WalletError, type WalletProviderPort, type WrapKey, WrongNetworkError, ZERO_ADDRESS, ZERO_HASH, aesGcmDecrypt, aesGcmEncrypt, buildSignedBatchMessage, computeCallsHash, createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, evmSign, evmVerify, generateNonce, getAuthNonce, getRandomBytes, hkdfSha256, normalizeWalletError, sealMasterSeed, selectSigner, selectSigner$1 as selectSignerLegacy, signAuthorization, signSession, toChecksumAddress, unsealMasterSeed, uploadBlob, uploadBlobViaPresign, zeroize };
package/dist/index.js CHANGED
@@ -73,6 +73,15 @@ function getRandomBytes(len) {
73
73
  }
74
74
 
75
75
  // src/core/crypto/aes-gcm.ts
76
+ function generateNonce() {
77
+ const nonce = new Uint8Array(12);
78
+ const timestamp = Math.floor(Date.now() / 1e3);
79
+ const view = new DataView(nonce.buffer);
80
+ view.setUint32(0, timestamp, false);
81
+ const random = getRandomBytes(8);
82
+ nonce.set(random, 4);
83
+ return nonce;
84
+ }
76
85
  function getSubtleCrypto() {
77
86
  if (typeof crypto !== "undefined" && crypto.subtle) {
78
87
  return crypto.subtle;
@@ -93,7 +102,7 @@ async function aesGcmEncrypt(plain, params) {
93
102
  if (key.length !== 32) {
94
103
  throw new Error(`${ERR_INVALID_PARAM}: key must be 32 bytes, got ${key.length}`);
95
104
  }
96
- const nonce = providedNonce || getRandomBytes(12);
105
+ const nonce = providedNonce || generateNonce();
97
106
  if (nonce.length !== 12) {
98
107
  throw new Error(`${ERR_NONCE_SIZE}: nonce must be 12 bytes, got ${nonce.length}`);
99
108
  }
@@ -1050,62 +1059,79 @@ async function getAuthNonce(client, from, mode) {
1050
1059
  }
1051
1060
  function createPasskeyProvider(passkey, options) {
1052
1061
  let unwrappedKeypair = null;
1053
- const ensureSession = async (opts) => {
1054
- if (opts?.force && unwrappedKeypair) {
1055
- console.log("[PasskeyProvider] force=true: zeroizing existing session");
1062
+ const sessionTtlMs = options.sessionTtlMs ?? 0;
1063
+ let sessionExpiresAt = null;
1064
+ let lockTimer = null;
1065
+ const isSessionExpired = () => {
1066
+ if (sessionTtlMs <= 0) return false;
1067
+ if (!sessionExpiresAt) return true;
1068
+ return Date.now() >= sessionExpiresAt;
1069
+ };
1070
+ const resetSessionTimer = () => {
1071
+ if (sessionTtlMs <= 0) return;
1072
+ if (lockTimer) {
1073
+ clearTimeout(lockTimer);
1074
+ lockTimer = null;
1075
+ }
1076
+ sessionExpiresAt = Date.now() + sessionTtlMs;
1077
+ lockTimer = setTimeout(async () => {
1078
+ console.log("[PasskeyProvider] Session expired, auto-locking");
1079
+ await lock();
1080
+ }, sessionTtlMs);
1081
+ };
1082
+ const lock = async () => {
1083
+ if (lockTimer) {
1084
+ clearTimeout(lockTimer);
1085
+ lockTimer = null;
1086
+ }
1087
+ sessionExpiresAt = null;
1088
+ if (unwrappedKeypair) {
1089
+ console.log("[PasskeyProvider] lock(): zeroizing sensitive data from memory");
1056
1090
  zeroize(unwrappedKeypair.privateKey);
1057
1091
  zeroize(unwrappedKeypair.publicKey);
1058
1092
  unwrappedKeypair = null;
1059
1093
  }
1060
- if (unwrappedKeypair) {
1094
+ };
1095
+ const ensureSession = async (opts) => {
1096
+ if (opts?.force && unwrappedKeypair) {
1097
+ console.log("[PasskeyProvider] force=true: zeroizing existing session");
1098
+ await lock();
1099
+ }
1100
+ if (unwrappedKeypair && !isSessionExpired()) {
1101
+ resetSessionTimer();
1061
1102
  return;
1062
1103
  }
1063
- if (opts?.interactive || opts?.force) {
1064
- console.log("[PasskeyProvider] interactive mode: triggering WebAuthn prompt");
1065
- const prfSalt = deriveWrapKey(options.prfInput);
1066
- const { prfOutput } = await passkey.authenticate({
1067
- salt: prfSalt,
1068
- credentialId: options.prfInput.credentialId
1069
- });
1070
- const wrapKey = prfOutput;
1071
- const aad = options.aad || new TextEncoder().encode("volr/master-seed/v1");
1072
- const masterSeed = await unsealMasterSeed(
1073
- options.encryptedBlob.cipher,
1074
- wrapKey,
1075
- aad,
1076
- options.encryptedBlob.nonce
1077
- );
1078
- const keypair = deriveEvmKey({ masterSeed });
1079
- unwrappedKeypair = {
1080
- privateKey: keypair.privateKey,
1081
- publicKey: keypair.publicKey,
1082
- address: keypair.address
1083
- };
1084
- zeroize(masterSeed);
1085
- zeroize(wrapKey);
1086
- zeroize(prfSalt);
1087
- } else {
1088
- console.warn("[PasskeyProvider] Non-interactive mode: using deterministic wrap key (insecure for TTL=0)");
1089
- const wrapKey = deriveWrapKey(options.prfInput);
1090
- const aad = options.aad || new TextEncoder().encode("volr/master-seed/v1");
1091
- const masterSeed = await unsealMasterSeed(
1092
- options.encryptedBlob.cipher,
1093
- wrapKey,
1094
- aad,
1095
- options.encryptedBlob.nonce
1096
- );
1097
- const keypair = deriveEvmKey({ masterSeed });
1098
- unwrappedKeypair = {
1099
- privateKey: keypair.privateKey,
1100
- publicKey: keypair.publicKey,
1101
- address: keypair.address
1102
- };
1103
- zeroize(masterSeed);
1104
- zeroize(wrapKey);
1104
+ if (unwrappedKeypair && isSessionExpired()) {
1105
+ console.log("[PasskeyProvider] Session expired, re-authenticating");
1106
+ await lock();
1105
1107
  }
1108
+ console.log("[PasskeyProvider] Triggering WebAuthn prompt for hardware-backed authentication");
1109
+ const prfSalt = deriveWrapKey(options.prfInput);
1110
+ const { prfOutput } = await passkey.authenticate({
1111
+ salt: prfSalt,
1112
+ credentialId: options.prfInput.credentialId
1113
+ });
1114
+ const wrapKey = prfOutput;
1115
+ const aad = options.aad || new TextEncoder().encode("volr/master-seed/v1");
1116
+ const masterSeed = await unsealMasterSeed(
1117
+ options.encryptedBlob.cipher,
1118
+ wrapKey,
1119
+ aad,
1120
+ options.encryptedBlob.nonce
1121
+ );
1122
+ const keypair = deriveEvmKey({ masterSeed });
1123
+ unwrappedKeypair = {
1124
+ privateKey: keypair.privateKey,
1125
+ publicKey: keypair.publicKey,
1126
+ address: keypair.address
1127
+ };
1128
+ zeroize(masterSeed);
1129
+ zeroize(wrapKey);
1130
+ zeroize(prfSalt);
1131
+ resetSessionTimer();
1106
1132
  };
1107
1133
  const getAddress = async () => {
1108
- await ensureSession();
1134
+ await ensureSession({ });
1109
1135
  if (!unwrappedKeypair) {
1110
1136
  throw new Error(`${ERR_INVALID_PARAM}: Session not established`);
1111
1137
  }
@@ -1115,7 +1141,7 @@ function createPasskeyProvider(passkey, options) {
1115
1141
  if (hash32.length !== 32) {
1116
1142
  throw new Error(`${ERR_INVALID_PARAM}: Message hash must be 32 bytes, got ${hash32.length}`);
1117
1143
  }
1118
- await ensureSession();
1144
+ await ensureSession({ });
1119
1145
  if (!unwrappedKeypair) {
1120
1146
  throw new Error(`${ERR_INVALID_PARAM}: Session not established`);
1121
1147
  }
@@ -1127,7 +1153,7 @@ function createPasskeyProvider(passkey, options) {
1127
1153
  };
1128
1154
  };
1129
1155
  const signTypedData = async (input) => {
1130
- await ensureSession();
1156
+ await ensureSession({ });
1131
1157
  if (!unwrappedKeypair) {
1132
1158
  throw new Error(`${ERR_INVALID_PARAM}: Session not established`);
1133
1159
  }
@@ -1143,7 +1169,7 @@ function createPasskeyProvider(passkey, options) {
1143
1169
  for (let i = 0; i < 32; i++) {
1144
1170
  msgHashBytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1145
1171
  }
1146
- const sig = await signMessage(msgHashBytes);
1172
+ const sig = evmSign(unwrappedKeypair.privateKey, msgHashBytes);
1147
1173
  const v = sig.yParity + 27;
1148
1174
  const rHex = Array.from(sig.r).map((b) => b.toString(16).padStart(2, "0")).join("");
1149
1175
  const sHex = Array.from(sig.s).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -1151,20 +1177,12 @@ function createPasskeyProvider(passkey, options) {
1151
1177
  return `0x${rHex}${sHex}${vHex}`;
1152
1178
  };
1153
1179
  const getPublicKey = async () => {
1154
- await ensureSession();
1180
+ await ensureSession({ });
1155
1181
  if (!unwrappedKeypair) {
1156
1182
  throw new Error(`${ERR_INVALID_PARAM}: Session not established`);
1157
1183
  }
1158
1184
  return new Uint8Array(unwrappedKeypair.publicKey);
1159
1185
  };
1160
- const lock = async () => {
1161
- if (unwrappedKeypair) {
1162
- console.log("[PasskeyProvider] lock(): zeroizing sensitive data from memory");
1163
- zeroize(unwrappedKeypair.privateKey);
1164
- zeroize(unwrappedKeypair.publicKey);
1165
- unwrappedKeypair = null;
1166
- }
1167
- };
1168
1186
  return {
1169
1187
  keyStorageType: "passkey",
1170
1188
  ensureSession,
@@ -1283,6 +1301,6 @@ async function selectSigner2(ctx) {
1283
1301
  );
1284
1302
  }
1285
1303
 
1286
- export { ChainNotAddedError, DEFAULT_EVM_PATH, DOMAIN, EIP1193ErrorCode, EIP712_DOMAIN, ERR_AAD_MISMATCH, ERR_CHAIN_MISMATCH, ERR_CRYPTO_FAIL, ERR_INVALID_PARAM, ERR_LOW_ENTROPY, ERR_NONCE_SIZE, ExternalWalletSigner, PasskeyP256Signer, RequestPendingError, Secp256k1SoftwareSigner, TYPES, UnauthorizedError, UnsupportedMethodError, UserRejectedError, VolrError, WalletError, WrongNetworkError, ZERO_ADDRESS, ZERO_HASH, aesGcmDecrypt, aesGcmEncrypt, buildSignedBatchMessage, computeCallsHash, createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, evmSign, evmVerify, getAuthNonce, getRandomBytes, hkdfSha256, normalizeWalletError, sealMasterSeed, selectSigner, selectSigner2 as selectSignerLegacy, signAuthorization, signSession, toChecksumAddress, unsealMasterSeed, uploadBlob, uploadBlobViaPresign, zeroize };
1304
+ export { ChainNotAddedError, DEFAULT_EVM_PATH, DOMAIN, EIP1193ErrorCode, EIP712_DOMAIN, ERR_AAD_MISMATCH, ERR_CHAIN_MISMATCH, ERR_CRYPTO_FAIL, ERR_INVALID_PARAM, ERR_LOW_ENTROPY, ERR_NONCE_SIZE, ExternalWalletSigner, PasskeyP256Signer, RequestPendingError, Secp256k1SoftwareSigner, TYPES, UnauthorizedError, UnsupportedMethodError, UserRejectedError, VolrError, WalletError, WrongNetworkError, ZERO_ADDRESS, ZERO_HASH, aesGcmDecrypt, aesGcmEncrypt, buildSignedBatchMessage, computeCallsHash, createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, evmSign, evmVerify, generateNonce, getAuthNonce, getRandomBytes, hkdfSha256, normalizeWalletError, sealMasterSeed, selectSigner, selectSigner2 as selectSignerLegacy, signAuthorization, signSession, toChecksumAddress, unsealMasterSeed, uploadBlob, uploadBlobViaPresign, zeroize };
1287
1305
  //# sourceMappingURL=index.js.map
1288
1306
  //# sourceMappingURL=index.js.map