@sidhujag/sysweb3-keyring 1.0.611 → 1.0.613
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/README.md +39 -1
- package/cjs/keyring-manager.js +239 -78
- package/cjs/keyring-manager.js.map +1 -1
- package/cjs/ledger/bitcoin_client/lib/psbtv2.js +11 -2
- package/cjs/ledger/bitcoin_client/lib/psbtv2.js.map +1 -1
- package/cjs/ledger/index.js +4 -0
- package/cjs/ledger/index.js.map +1 -1
- package/cjs/storage.js +86 -27
- package/cjs/storage.js.map +1 -1
- package/cjs/transactions/ethereum.js +21 -1
- package/cjs/transactions/ethereum.js.map +1 -1
- package/cjs/transactions/evm-local-signer.js +2 -1
- package/cjs/transactions/evm-local-signer.js.map +1 -1
- package/cjs/transactions/syscoin.js +108 -5
- package/cjs/transactions/syscoin.js.map +1 -1
- package/cjs/trezor/index.js +115 -31
- package/cjs/trezor/index.js.map +1 -1
- package/cjs/utils/psbt-account-scope.js +347 -0
- package/cjs/utils/psbt-account-scope.js.map +1 -0
- package/package.json +3 -2
- package/types/keyring-manager.d.ts +9 -0
- package/types/storage.d.ts +8 -2
- package/types/transactions/ethereum.d.ts +1 -0
- package/types/transactions/evm-local-signer.d.ts +1 -1
- package/types/transactions/syscoin.d.ts +2 -1
- package/types/utils/psbt-account-scope.d.ts +17 -0
package/README.md
CHANGED
|
@@ -14,6 +14,40 @@ The sysweb3-keyring provides a unified interface for managing accounts, transact
|
|
|
14
14
|
- **Secure Session Management**: Encrypted private key handling with session transfer
|
|
15
15
|
- **Transaction Management**: Full transaction lifecycle support for both network types
|
|
16
16
|
|
|
17
|
+
## Vault encryption compatibility
|
|
18
|
+
|
|
19
|
+
New vaults require WebCrypto and use PBKDF2-HMAC-SHA-512 (900,000 production
|
|
20
|
+
iterations) with AES-256-GCM. Missing WebCrypto rejects the operation without
|
|
21
|
+
writing a weaker fallback vault. Restore WebCrypto availability before retrying
|
|
22
|
+
an unmarked legacy vault.
|
|
23
|
+
|
|
24
|
+
Historical fallback vaults used a 20,000-iteration key that also encrypted their
|
|
25
|
+
persisted account keys. After authenticating such a CBC vault, the keyring records
|
|
26
|
+
the explicit `pbkdf2-sha512-20000` compatibility profile and rewraps the vault with
|
|
27
|
+
AES-GCM. This preserves existing account access across restart; it is **not** a
|
|
28
|
+
full rekey of those account records to 900,000 iterations. The profile is written
|
|
29
|
+
before rewrapping, so interruption of either write remains recoverable. Arbitrary
|
|
30
|
+
KDF profiles or parameters are rejected.
|
|
31
|
+
|
|
32
|
+
Storage clients must return their asynchronous write/delete promises. The core
|
|
33
|
+
adapter preserves them so rejected writes do not silently advance vault migration.
|
|
34
|
+
Fresh wallet creation additionally requires conditional batch creation. Native
|
|
35
|
+
`chrome.storage.local` and `browser.storage.local` use a prefix-scoped Web Lock
|
|
36
|
+
around the absence check and awaited write, coordinating contexts in the same
|
|
37
|
+
extension origin and storage partition. Custom clients must provide an atomic
|
|
38
|
+
`createItemsIfAbsent(items)` backend operation over prefixed, JSON-serialized
|
|
39
|
+
values; the built-in memory client provides this operation synchronously.
|
|
40
|
+
There is no sequential or unlocked fallback. A backend shared across storage
|
|
41
|
+
partitions needs its own atomic implementation or a single owner.
|
|
42
|
+
|
|
43
|
+
### Release order for 1.0.613
|
|
44
|
+
|
|
45
|
+
Publish `@sidhujag/sysweb3-core@1.0.29` before `@sidhujag/sysweb3-keyring@1.0.613`.
|
|
46
|
+
The updated lock entry describes that intended dependency; its integrity is
|
|
47
|
+
intentionally absent until publication. Regenerate and verify the lockfile from
|
|
48
|
+
the published registry artifacts before a clean registry installation. Local
|
|
49
|
+
validation uses the built package tarballs and does not establish publication.
|
|
50
|
+
|
|
17
51
|
## Installation
|
|
18
52
|
|
|
19
53
|
```bash
|
|
@@ -191,7 +225,11 @@ Main class for keyring operations:
|
|
|
191
225
|
|
|
192
226
|
## Security
|
|
193
227
|
|
|
194
|
-
-
|
|
228
|
+
- `initializeSession`, `initializeWalletSecurely`, and the initialization factories create a new wallet only when both vault records are absent. Use `unlock()` to restore an existing wallet. Repeating initialization on a matching live session verifies the stored wallet without rewriting it.
|
|
229
|
+
- Fresh vault salt and ciphertext are created together only if both are absent, through the core conditional-creation operation. Native extension contexts sharing an origin/storage partition are coordinated by Web Locks; custom backends must provide atomic `createItemsIfAbsent`. Unsupported adapters are rejected before writing. Existing or incomplete records are preserved.
|
|
230
|
+
- UTXO signing authenticates the selected account's paths, public keys and spent scripts. A joint PSBT may include unfinished inputs for another signer: their HD/path hints are removed before the selected private signer runs, then their public metadata is restored to the returned partial PSBT. Another account in the same wallet remains unsigned. At least one unfinished input must authenticate to the selected account; a wholly finalized PSBT retains its existing handling. Standard P2WSH, wrapped P2WSH and P2SH multisig preserve external cosigner signatures and continuation. Hardware paths and single-address imports can reject unsupported joint inputs rather than widening signing authority.
|
|
231
|
+
- Hardware signing preserves already-finalized external inputs without requesting their private derivations. Trezor receives them as `EXTERNAL` inputs; Ledger retains their final scripts in PSBTv2 and skips signing/finalizing them again. Unfinished foreign hardware inputs remain unsupported.
|
|
232
|
+
- Persisted wallet and account secrets are encrypted; plaintext signing material is used in memory
|
|
195
233
|
- Session data is cleared when the keyring is locked
|
|
196
234
|
- Hardware wallet integration follows device security models
|
|
197
235
|
- Secure memory management with explicit cleanup
|
package/cjs/keyring-manager.js
CHANGED
|
@@ -51,6 +51,7 @@ const BIP84 = __importStar(require("syscoinjs-lib/bip84-replacement"));
|
|
|
51
51
|
// Reference embedded bitcoinjs from syscoinjs-lib
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
53
|
const bjs = syscoinjs.utils.bitcoinjs;
|
|
54
|
+
const LEGACY_ENCRYPTION_KDF = 'pbkdf2-sha512-20000';
|
|
54
55
|
const SLH_DSA_DERIVATION_VERSION = 1;
|
|
55
56
|
// Preserve Pali v1 SLH derivation: first 32 bytes of the account-scoped HMAC.
|
|
56
57
|
// Pali expands this setup secret into the 48-byte SLH skSeed/skPrf/pkSeed tuple.
|
|
@@ -68,6 +69,7 @@ const trezor_1 = require("./trezor");
|
|
|
68
69
|
const types_1 = require("./types");
|
|
69
70
|
const blockbook_cache_1 = require("./utils/blockbook-cache");
|
|
70
71
|
const derivation_paths_1 = require("./utils/derivation-paths");
|
|
72
|
+
const psbt_account_scope_1 = require("./utils/psbt-account-scope");
|
|
71
73
|
// Dynamic ETH HD path generation - will be computed as needed
|
|
72
74
|
/**
|
|
73
75
|
* Secure Buffer implementation for sensitive data
|
|
@@ -97,9 +99,7 @@ class SecureBuffer {
|
|
|
97
99
|
}
|
|
98
100
|
clear() {
|
|
99
101
|
if (!this._isCleared && this.buffer) {
|
|
100
|
-
//
|
|
101
|
-
crypto_1.default.randomFillSync(this.buffer);
|
|
102
|
-
// Then fill with zeros
|
|
102
|
+
// Zeroing must not depend on a working platform random source.
|
|
103
103
|
this.buffer.fill(0);
|
|
104
104
|
this.buffer = null;
|
|
105
105
|
this._isCleared = true;
|
|
@@ -376,7 +376,7 @@ class KeyringManager {
|
|
|
376
376
|
if (!vaultKeys || !vaultKeys.salt) {
|
|
377
377
|
throw new Error('Vault keys not found');
|
|
378
378
|
}
|
|
379
|
-
const genPwd = await this.
|
|
379
|
+
const genPwd = await this.deriveStoredSessionKey(pwd, vaultKeys);
|
|
380
380
|
if (this.getSessionPasswordString() !== genPwd) {
|
|
381
381
|
throw new Error('Invalid password');
|
|
382
382
|
}
|
|
@@ -428,7 +428,7 @@ class KeyringManager {
|
|
|
428
428
|
if (!vaultKeys || !vaultKeys.salt) {
|
|
429
429
|
throw new Error('Vault keys not found');
|
|
430
430
|
}
|
|
431
|
-
const genPwd = await this.
|
|
431
|
+
const genPwd = await this.deriveStoredSessionKey(pwd, vaultKeys);
|
|
432
432
|
if (this.getSessionPasswordString() !== genPwd) {
|
|
433
433
|
throw new Error('Invalid password');
|
|
434
434
|
}
|
|
@@ -560,7 +560,7 @@ class KeyringManager {
|
|
|
560
560
|
if (!vaultKeys || !vaultKeys.salt) {
|
|
561
561
|
throw new Error('Vault keys not found');
|
|
562
562
|
}
|
|
563
|
-
const genPwd = await this.
|
|
563
|
+
const genPwd = await this.deriveStoredSessionKey(pwd, vaultKeys);
|
|
564
564
|
if (!this.sessionPassword) {
|
|
565
565
|
throw new Error('Unlock wallet first');
|
|
566
566
|
}
|
|
@@ -757,6 +757,33 @@ class KeyringManager {
|
|
|
757
757
|
throw error;
|
|
758
758
|
}
|
|
759
759
|
};
|
|
760
|
+
this.capturePsbtSigningContext = () => {
|
|
761
|
+
const session = this.sessionPassword;
|
|
762
|
+
const vault = this.getVault();
|
|
763
|
+
const selected = { ...vault.activeAccount };
|
|
764
|
+
const account = vault.accounts[selected.type]?.[selected.id];
|
|
765
|
+
const identity = { address: account?.address, xpub: account?.xpub };
|
|
766
|
+
const network = { ...vault.activeNetwork };
|
|
767
|
+
const assertCurrent = () => {
|
|
768
|
+
const current = this.getVault();
|
|
769
|
+
const currentAccount = current.accounts[selected.type]?.[selected.id];
|
|
770
|
+
if (!session ||
|
|
771
|
+
session.isCleared() ||
|
|
772
|
+
this.sessionPassword !== session ||
|
|
773
|
+
current.activeAccount.id !== selected.id ||
|
|
774
|
+
current.activeAccount.type !== selected.type ||
|
|
775
|
+
currentAccount?.address !== identity.address ||
|
|
776
|
+
currentAccount?.xpub !== identity.xpub ||
|
|
777
|
+
current.activeNetwork.kind !== network.kind ||
|
|
778
|
+
current.activeNetwork.chainId !== network.chainId ||
|
|
779
|
+
current.activeNetwork.slip44 !== network.slip44 ||
|
|
780
|
+
current.activeNetwork.url !== network.url) {
|
|
781
|
+
throw new Error('Wallet signing context changed');
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
assertCurrent();
|
|
785
|
+
return assertCurrent;
|
|
786
|
+
};
|
|
760
787
|
this.getSigner = () => {
|
|
761
788
|
if (!this.sessionPassword) {
|
|
762
789
|
throw new Error('Wallet is locked cant proceed with transaction');
|
|
@@ -768,6 +795,7 @@ class KeyringManager {
|
|
|
768
795
|
const { activeAccount } = vault;
|
|
769
796
|
const accountId = activeAccount.id;
|
|
770
797
|
const accountType = activeAccount.type;
|
|
798
|
+
const assertCurrent = this.capturePsbtSigningContext();
|
|
771
799
|
// Determine signer type: HD or WIF single-address
|
|
772
800
|
let signerForUse;
|
|
773
801
|
if (accountType === types_1.KeyringAccountType.HDAccount) {
|
|
@@ -803,6 +831,33 @@ class KeyringManager {
|
|
|
803
831
|
else {
|
|
804
832
|
throw new Error(`Unsupported account type for UTXO signing: ${accountType}`);
|
|
805
833
|
}
|
|
834
|
+
const scope = {
|
|
835
|
+
account: vault.accounts[accountType][accountId],
|
|
836
|
+
accountId,
|
|
837
|
+
accountType,
|
|
838
|
+
network: vault.activeNetwork,
|
|
839
|
+
};
|
|
840
|
+
if (typeof signerForUse.getRootNode === 'function') {
|
|
841
|
+
const getRootNode = signerForUse.getRootNode.bind(signerForUse);
|
|
842
|
+
const guardPath = (0, psbt_account_scope_1.createPsbtDerivationGuard)(scope);
|
|
843
|
+
signerForUse.getRootNode = () => {
|
|
844
|
+
const root = getRootNode();
|
|
845
|
+
const scopedRoot = Object.create(root);
|
|
846
|
+
scopedRoot.derivePath = (path) => {
|
|
847
|
+
guardPath(path);
|
|
848
|
+
return root.derivePath(path);
|
|
849
|
+
};
|
|
850
|
+
return scopedRoot;
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const sign = signerForUse.sign.bind(signerForUse);
|
|
854
|
+
signerForUse.sign = async (psbt) => {
|
|
855
|
+
assertCurrent();
|
|
856
|
+
(0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
|
|
857
|
+
const result = await sign(psbt);
|
|
858
|
+
assertCurrent();
|
|
859
|
+
return result;
|
|
860
|
+
};
|
|
806
861
|
// Create syscoinjs instance with current network (no need to attach signer for signing flow)
|
|
807
862
|
const network = vault.activeNetwork;
|
|
808
863
|
const networkConfig = (0, sysweb3_network_1.getNetworkConfig)(network.slip44, network.currency);
|
|
@@ -1024,51 +1079,65 @@ class KeyringManager {
|
|
|
1024
1079
|
if (!BIP84.validateMnemonic(seedPhrase)) {
|
|
1025
1080
|
throw new Error('Invalid Seed');
|
|
1026
1081
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (sessionPasswordSaltedHash === this.getSessionPasswordString()) {
|
|
1082
|
+
const [vault, vaultKeys] = await Promise.all([
|
|
1083
|
+
this.storage.get('vault'),
|
|
1084
|
+
this.storage.get('vault-keys'),
|
|
1085
|
+
]);
|
|
1086
|
+
const hasVault = vault !== undefined && vault !== null;
|
|
1087
|
+
const hasVaultKeys = vaultKeys !== undefined && vaultKeys !== null;
|
|
1088
|
+
const livePassword = this.sessionPassword;
|
|
1089
|
+
const liveMnemonic = this.sessionMnemonic;
|
|
1090
|
+
// Only an already-unlocked, matching session may repeat initialization.
|
|
1091
|
+
// Restoring any existing wallet must use unlock(), never rewrite its vault.
|
|
1092
|
+
if (livePassword &&
|
|
1093
|
+
liveMnemonic &&
|
|
1094
|
+
!livePassword.isCleared() &&
|
|
1095
|
+
!liveMnemonic.isCleared()) {
|
|
1096
|
+
if (!hasVault || !hasVaultKeys || !vaultKeys.salt) {
|
|
1097
|
+
throw new Error('Cannot initialize a new vault over existing wallet storage');
|
|
1098
|
+
}
|
|
1099
|
+
const sessionPasswordSaltedHash = await this.deriveStoredSessionKey(password, vaultKeys);
|
|
1100
|
+
if (livePassword === this.sessionPassword &&
|
|
1101
|
+
liveMnemonic === this.sessionMnemonic &&
|
|
1102
|
+
sessionPasswordSaltedHash === this.getSessionPasswordString()) {
|
|
1049
1103
|
// Same password - check if it's the same mnemonic to ensure full idempotency
|
|
1104
|
+
let currentMnemonic;
|
|
1050
1105
|
try {
|
|
1051
|
-
|
|
1106
|
+
currentMnemonic = this.withSecureData((sessionPwd, sessionMnemonic) => {
|
|
1052
1107
|
return crypto_js_1.default.AES.decrypt(sessionMnemonic, sessionPwd).toString(crypto_js_1.default.enc.Utf8);
|
|
1053
1108
|
});
|
|
1054
|
-
if (currentMnemonic === seedPhrase) {
|
|
1055
|
-
// Same mnemonic and password - already initialized
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
1109
|
}
|
|
1059
1110
|
catch (error) {
|
|
1060
1111
|
// If we can't decrypt, fall through to error
|
|
1061
1112
|
}
|
|
1113
|
+
if (currentMnemonic === seedPhrase) {
|
|
1114
|
+
const storedVault = await (0, storage_1.getDecryptedVault)(sessionPasswordSaltedHash);
|
|
1115
|
+
if (livePassword === this.sessionPassword &&
|
|
1116
|
+
liveMnemonic === this.sessionMnemonic &&
|
|
1117
|
+
!livePassword.isCleared() &&
|
|
1118
|
+
!liveMnemonic.isCleared() &&
|
|
1119
|
+
storedVault?.mnemonic === seedPhrase) {
|
|
1120
|
+
// This is a read-only identity check, not a second vault write.
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1062
1124
|
}
|
|
1063
1125
|
// Different password or mnemonic - this is not a simple re-initialization
|
|
1064
|
-
throw new Error('Wallet already initialized with different parameters.
|
|
1126
|
+
throw new Error('Wallet already initialized with different parameters. Unlock the existing wallet or explicitly remove it before starting setup.');
|
|
1065
1127
|
}
|
|
1128
|
+
if (hasVault || hasVaultKeys) {
|
|
1129
|
+
throw new Error('Cannot initialize a new vault over existing wallet storage');
|
|
1130
|
+
}
|
|
1131
|
+
const salt = crypto_1.default.randomBytes(16).toString('hex');
|
|
1132
|
+
// v4 stores only the salt on disk. A genuinely fresh initialization always
|
|
1133
|
+
// commits its own salt and ciphertext together under the vault mutex.
|
|
1134
|
+
const sessionPasswordSaltedHash = await this.deriveStoredSessionKey(password, { salt });
|
|
1066
1135
|
// Encrypt and store vault (mnemonic storage) - now uses single vault for all networks
|
|
1067
1136
|
await (0, storage_1.setEncryptedVault)({
|
|
1068
1137
|
mnemonic: seedPhrase, // Store plain mnemonic - setEncryptedVault will encrypt the entire vault
|
|
1069
1138
|
},
|
|
1070
1139
|
// v4 vault is encrypted with the derived session password key (PBKDF2 output)
|
|
1071
|
-
sessionPasswordSaltedHash);
|
|
1140
|
+
sessionPasswordSaltedHash, { salt });
|
|
1072
1141
|
await this.recreateSessionFromVault(sessionPasswordSaltedHash);
|
|
1073
1142
|
};
|
|
1074
1143
|
// NEW: Create first account without signer setup
|
|
@@ -1110,9 +1179,7 @@ class KeyringManager {
|
|
|
1110
1179
|
};
|
|
1111
1180
|
this.storage = sysweb3.sysweb3Di.getStateStorageDb();
|
|
1112
1181
|
// Don't initialize secure buffers in constructor - they're created on unlock
|
|
1113
|
-
this.
|
|
1114
|
-
hasUtf8Error: false,
|
|
1115
|
-
});
|
|
1182
|
+
this.writeUtf8Diagnostic(false);
|
|
1116
1183
|
// NOTE: activeChain is now derived from vault state, not stored locally
|
|
1117
1184
|
// NOTE: No more persistent signers - use getSigner() for fresh on-demand signers
|
|
1118
1185
|
this.utf8Error = false;
|
|
@@ -1125,7 +1192,7 @@ class KeyringManager {
|
|
|
1125
1192
|
this.trezorSigner = new trezor_1.TrezorKeyring();
|
|
1126
1193
|
this.ledgerSigner = new ledger_1.LedgerKeyring(hardwareManager);
|
|
1127
1194
|
// this.syscoinTransaction = SyscoinTransactions();
|
|
1128
|
-
this.syscoinTransaction = new transactions_1.SyscoinTransactions(this.getSigner, this.getReadOnlySigner, this.getAccountsState, this.getAddress, this.ledgerSigner, this.trezorSigner);
|
|
1195
|
+
this.syscoinTransaction = new transactions_1.SyscoinTransactions(this.getSigner, this.getReadOnlySigner, this.getAccountsState, this.getAddress, this.ledgerSigner, this.trezorSigner, this.capturePsbtSigningContext);
|
|
1129
1196
|
this.ethereumTransaction = new transactions_1.EthereumTransactions(this.getNetwork, this.getDecryptedPrivateKey, this.getAccountsState, this.ledgerSigner, this.trezorSigner);
|
|
1130
1197
|
}
|
|
1131
1198
|
// Static factory method for creating a fully initialized KeyringManager with slip44 support
|
|
@@ -1151,56 +1218,75 @@ class KeyringManager {
|
|
|
1151
1218
|
// No need to explicitly set active account - it's managed externally
|
|
1152
1219
|
return account;
|
|
1153
1220
|
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Returns canLogin:false only when the first vault authentication fails.
|
|
1223
|
+
* Storage, platform and session restoration failures reject so callers do
|
|
1224
|
+
* not charge a failed password attempt for an operational error.
|
|
1225
|
+
*/
|
|
1154
1226
|
async unlock(password) {
|
|
1227
|
+
let authenticated = false;
|
|
1155
1228
|
try {
|
|
1156
1229
|
const vaultKeys = await this.storage.get('vault-keys');
|
|
1157
1230
|
if (!vaultKeys) {
|
|
1158
|
-
|
|
1159
|
-
canLogin: false,
|
|
1160
|
-
};
|
|
1231
|
+
throw new Error('Vault keys not found. Reload the wallet and try again.');
|
|
1161
1232
|
}
|
|
1162
1233
|
// v4: Validate password by deriving the session key once and decrypting the vault with it.
|
|
1163
1234
|
// This avoids an extra KDF pass just for a stored verifier.
|
|
1164
1235
|
const { salt } = vaultKeys;
|
|
1165
1236
|
// Derive session encryption key (in-memory only) used to encrypt/decrypt xprv fields
|
|
1166
1237
|
// IMPORTANT: This MUST NOT be the same value as the stored verifier above.
|
|
1167
|
-
|
|
1238
|
+
let sessionPasswordKey = await this.deriveStoredSessionKey(password, vaultKeys);
|
|
1168
1239
|
// Handle migration from old vault format with currentSessionSalt
|
|
1169
1240
|
if (vaultKeys.currentSessionSalt) {
|
|
1170
1241
|
console.log('[KeyringManager] Detected old vault format, handling session migration...');
|
|
1171
1242
|
// The old format used currentSessionSalt for session data encryption
|
|
1172
1243
|
// We need to use it temporarily to decrypt the mnemonic correctly
|
|
1173
1244
|
const oldSessionPassword = this.encryptSHA512(password, vaultKeys.currentSessionSalt);
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
if
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (
|
|
1245
|
+
// v3 CBC vaults use the raw password; migrated GCM vaults use the
|
|
1246
|
+
// derived key. Selecting by stored format also permits a safe retry
|
|
1247
|
+
// if the vault write succeeded but the metadata write was interrupted.
|
|
1248
|
+
const { mnemonic } = await (0, storage_1.getDecryptedVault)(sessionPasswordKey, password);
|
|
1249
|
+
authenticated = true;
|
|
1250
|
+
const isValidSecret = (value) => {
|
|
1251
|
+
if (typeof value !== 'string' || !value)
|
|
1252
|
+
return false;
|
|
1253
|
+
if (this.isSeedValid(value))
|
|
1254
|
+
return true;
|
|
1255
|
+
// Imported extended private keys can also be stored as the secret.
|
|
1256
|
+
if (!/^(xprv|yprv|zprv|tprv|uprv|vprv)/.test(value))
|
|
1257
|
+
return false;
|
|
1258
|
+
try {
|
|
1259
|
+
const decoded = bs58check_1.default.decode(value);
|
|
1260
|
+
return (decoded.length === 78 &&
|
|
1261
|
+
decoded[45] === 0 &&
|
|
1262
|
+
secp256k1_1.default.isPrivate(decoded.subarray(46)));
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
let decryptedMnemonic = mnemonic;
|
|
1269
|
+
if (!isValidSecret(decryptedMnemonic) && typeof mnemonic === 'string') {
|
|
1270
|
+
for (const legacyKey of [password, oldSessionPassword]) {
|
|
1183
1271
|
try {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
console.warn('[KeyringManager] Failed to decrypt mnemonic with password, trying old session password');
|
|
1189
|
-
// If that fails, try with old session password
|
|
1190
|
-
try {
|
|
1191
|
-
decryptedMnemonic = crypto_js_1.default.AES.decrypt(mnemonic, oldSessionPassword).toString(crypto_js_1.default.enc.Utf8);
|
|
1192
|
-
}
|
|
1193
|
-
catch (e2) {
|
|
1194
|
-
// If both fail, assume it's already decrypted
|
|
1195
|
-
decryptedMnemonic = mnemonic;
|
|
1272
|
+
const candidate = crypto_js_1.default.AES.decrypt(mnemonic, legacyKey).toString(crypto_js_1.default.enc.Utf8);
|
|
1273
|
+
if (isValidSecret(candidate)) {
|
|
1274
|
+
decryptedMnemonic = candidate;
|
|
1275
|
+
break;
|
|
1196
1276
|
}
|
|
1197
1277
|
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
if (error?.message !== 'Malformed UTF-8 data')
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1198
1282
|
}
|
|
1199
|
-
// Re-save the vault with properly formatted mnemonic (single encryption)
|
|
1200
|
-
// v4 vault is encrypted with the derived session password key (PBKDF2 output)
|
|
1201
|
-
await (0, storage_1.setEncryptedVault)({ mnemonic: decryptedMnemonic }, sessionPasswordKey);
|
|
1202
|
-
console.log('[KeyringManager] Vault mnemonic format normalized');
|
|
1203
1283
|
}
|
|
1284
|
+
if (!isValidSecret(decryptedMnemonic)) {
|
|
1285
|
+
throw new Error('Legacy vault secret could not be recovered. Existing vault preserved.');
|
|
1286
|
+
}
|
|
1287
|
+
// Do not change either stored record until a valid secret is recovered.
|
|
1288
|
+
await (0, storage_1.setEncryptedVault)({ mnemonic: decryptedMnemonic }, sessionPasswordKey);
|
|
1289
|
+
console.log('[KeyringManager] Vault mnemonic format normalized');
|
|
1204
1290
|
// Remove currentSessionSalt from vault-keys (v4 keeps only the salt on disk)
|
|
1205
1291
|
const migratedVaultKeys = {
|
|
1206
1292
|
salt: vaultKeys.salt,
|
|
@@ -1210,7 +1296,44 @@ class KeyringManager {
|
|
|
1210
1296
|
}
|
|
1211
1297
|
// v4 vault is encrypted with the derived session password key (PBKDF2 output).
|
|
1212
1298
|
// If the password is wrong, this will throw and we return canLogin:false below.
|
|
1213
|
-
|
|
1299
|
+
let decryptedVault;
|
|
1300
|
+
let legacyProfile = vaultKeys.keyDerivation === LEGACY_ENCRYPTION_KDF;
|
|
1301
|
+
try {
|
|
1302
|
+
decryptedVault = await (0, storage_1.getDecryptedVault)(sessionPasswordKey);
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
// Only the historical CBC fallback used the 20k key without metadata.
|
|
1306
|
+
// Never try it for GCM, malformed records, or platform/storage failures.
|
|
1307
|
+
const rawVault = error instanceof storage_1.VaultAuthenticationError &&
|
|
1308
|
+
!vaultKeys.keyDerivation &&
|
|
1309
|
+
!vaultKeys.currentSessionSalt
|
|
1310
|
+
? await this.storage.get('vault')
|
|
1311
|
+
: undefined;
|
|
1312
|
+
if (typeof rawVault !== 'string' || rawVault.trim().startsWith('{')) {
|
|
1313
|
+
throw error;
|
|
1314
|
+
}
|
|
1315
|
+
const legacyKey = await this.deriveLegacySessionKey(password, salt);
|
|
1316
|
+
decryptedVault = await (0, storage_1.getDecryptedVault)(legacyKey);
|
|
1317
|
+
authenticated = true;
|
|
1318
|
+
sessionPasswordKey = legacyKey;
|
|
1319
|
+
// Keep the historical account wrapping key. Persist its profile before
|
|
1320
|
+
// GCM rewrapping so either interrupted write remains recoverable.
|
|
1321
|
+
await this.storage.set('vault-keys', {
|
|
1322
|
+
...vaultKeys,
|
|
1323
|
+
keyDerivation: LEGACY_ENCRYPTION_KDF,
|
|
1324
|
+
});
|
|
1325
|
+
legacyProfile = true;
|
|
1326
|
+
}
|
|
1327
|
+
authenticated = true;
|
|
1328
|
+
if (legacyProfile && this.hasWebCrypto()) {
|
|
1329
|
+
const storedVault = await this.storage.get('vault');
|
|
1330
|
+
if (typeof storedVault === 'string' &&
|
|
1331
|
+
!storedVault.trim().startsWith('{')) {
|
|
1332
|
+
// A failed earlier rewrap can leave the authenticated profile plus
|
|
1333
|
+
// CBC. Retry that write without changing the account wrapping key.
|
|
1334
|
+
await (0, storage_1.setEncryptedVault)(decryptedVault, sessionPasswordKey);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1214
1337
|
// If session data missing or corrupted, recreate from vault
|
|
1215
1338
|
if (!this.sessionMnemonic) {
|
|
1216
1339
|
await this.recreateSessionFromVault(sessionPasswordKey);
|
|
@@ -1220,7 +1343,7 @@ class KeyringManager {
|
|
|
1220
1343
|
// so we must restore the real class instance on unlock.
|
|
1221
1344
|
if (!this.syscoinTransaction ||
|
|
1222
1345
|
typeof this.syscoinTransaction.getRecommendedFee !== 'function') {
|
|
1223
|
-
this.syscoinTransaction = new transactions_1.SyscoinTransactions(this.getSigner, this.getReadOnlySigner, this.getAccountsState, this.getAddress, this.ledgerSigner, this.trezorSigner);
|
|
1346
|
+
this.syscoinTransaction = new transactions_1.SyscoinTransactions(this.getSigner, this.getReadOnlySigner, this.getAccountsState, this.getAddress, this.ledgerSigner, this.trezorSigner, this.capturePsbtSigningContext);
|
|
1224
1347
|
}
|
|
1225
1348
|
// NOTE: Active account management is now handled by vault state/Redux
|
|
1226
1349
|
// No need to explicitly set active account after unlock - it's managed externally
|
|
@@ -1245,12 +1368,15 @@ class KeyringManager {
|
|
|
1245
1368
|
};
|
|
1246
1369
|
}
|
|
1247
1370
|
catch (error) {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1371
|
+
if (!authenticated && error instanceof storage_1.VaultAuthenticationError) {
|
|
1372
|
+
return { canLogin: false };
|
|
1373
|
+
}
|
|
1374
|
+
if (authenticated) {
|
|
1375
|
+
// lockWallet clears secret buffers synchronously; hardware cleanup
|
|
1376
|
+
// is best effort and must not hide the original operational error.
|
|
1377
|
+
void this.lockWallet().catch(() => undefined);
|
|
1378
|
+
}
|
|
1379
|
+
throw error;
|
|
1254
1380
|
}
|
|
1255
1381
|
}
|
|
1256
1382
|
isWatchOnlyAccount(a) {
|
|
@@ -1555,11 +1681,20 @@ class KeyringManager {
|
|
|
1555
1681
|
return { isValid: false, message: error.message };
|
|
1556
1682
|
}
|
|
1557
1683
|
}
|
|
1684
|
+
writeUtf8Diagnostic(hasUtf8Error) {
|
|
1685
|
+
try {
|
|
1686
|
+
void Promise.resolve(this.storage.set('utf8Error', { hasUtf8Error })).catch(() => undefined);
|
|
1687
|
+
}
|
|
1688
|
+
catch {
|
|
1689
|
+
// This diagnostic must not interrupt authentication or become an
|
|
1690
|
+
// unhandled rejection when an asynchronous storage client is offline.
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1558
1693
|
validateAndHandleErrorByMessage(message) {
|
|
1559
1694
|
const utf8ErrorMessage = 'Malformed UTF-8 data';
|
|
1560
1695
|
if (message.includes(utf8ErrorMessage) ||
|
|
1561
1696
|
message.toLowerCase().includes(utf8ErrorMessage.toLowerCase())) {
|
|
1562
|
-
this.
|
|
1697
|
+
this.writeUtf8Diagnostic(true);
|
|
1563
1698
|
}
|
|
1564
1699
|
}
|
|
1565
1700
|
async pbkdf2WebCryptoHex(params) {
|
|
@@ -1587,7 +1722,33 @@ class KeyringManager {
|
|
|
1587
1722
|
hash: 'SHA-512',
|
|
1588
1723
|
});
|
|
1589
1724
|
}
|
|
1590
|
-
|
|
1725
|
+
throw new Error('WebCrypto is required for vault key derivation');
|
|
1726
|
+
}
|
|
1727
|
+
async deriveLegacySessionKey(password, salt) {
|
|
1728
|
+
if (this.hasWebCrypto()) {
|
|
1729
|
+
return this.pbkdf2WebCryptoHex({
|
|
1730
|
+
password,
|
|
1731
|
+
saltHex: salt,
|
|
1732
|
+
iterations: 20000,
|
|
1733
|
+
lengthBytes: 32,
|
|
1734
|
+
hash: 'SHA-512',
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
// Read compatibility only; new vault writes still require WebCrypto.
|
|
1738
|
+
return crypto_js_1.default.PBKDF2(password, crypto_js_1.default.enc.Hex.parse(salt), {
|
|
1739
|
+
keySize: 256 / 32,
|
|
1740
|
+
iterations: 20000,
|
|
1741
|
+
hasher: crypto_js_1.default.algo.SHA512,
|
|
1742
|
+
}).toString();
|
|
1743
|
+
}
|
|
1744
|
+
async deriveStoredSessionKey(password, vaultKeys) {
|
|
1745
|
+
if (vaultKeys.keyDerivation === LEGACY_ENCRYPTION_KDF) {
|
|
1746
|
+
return this.deriveLegacySessionKey(password, vaultKeys.salt);
|
|
1747
|
+
}
|
|
1748
|
+
if (vaultKeys.keyDerivation !== undefined) {
|
|
1749
|
+
throw new Error('Unsupported vault key derivation profile');
|
|
1750
|
+
}
|
|
1751
|
+
return this.encryptSHA512Async(password, vaultKeys.salt);
|
|
1591
1752
|
}
|
|
1592
1753
|
async _createTrezorAccount(coin, slip44, index, label) {
|
|
1593
1754
|
const vault = this.getVault();
|