Skip to content

Commit 7cb5032

Browse files
KtorZnikolaglumac
andauthored
[DDW-563] Re-enable wallet import feature (#2308)
* add utility code for decoding legacy keystores from cardano-sl Developed it as a standalone library, feel free to rip it apart the way you like. ``` restore-keystore ├── index.spec.ts ├── index.ts ├── jest.config.ts ├── LICENSE ├── NOTICE ├── package.json ├── package-lock.json └── __test__ └── secret.key ``` It's shipped with a default test keystore that I've been previously using on input-output-hk/cardano-sl#4278 and a test suite which controls that this new implementation yield the same results. ```console > restore-keystore@1.0.0 test /home/ktorz/Documents/IOHK/restore-keystore > jest PASS ./index.spec.ts walletId ✓ 0 (2 ms) ✓ 1 ✓ 2 (1 ms) ✓ 3 encryptedPayload ✓ 0 ✓ 1 (1 ms) ✓ 2 ✓ 3 passphraseHash ✓ 0 (1 ms) ✓ 1 ✓ 2 ✓ 3 isEmptyPassphrase ✓ 0 ✓ 1 ✓ 2 ✓ 3 Test Suites: 1 passed, 1 total Tests: 16 passed, 16 total Snapshots: 0 total Time: 2.3 s, estimated 3 s Ran all test suites. ``` The magic is yours now and should help resolving #1234. * add 'executable' to restore and show a keystore at a given path Can be used directly (after `npm install`) as an executable, e.g.: ``` $ ./restore-keystore.ts __test__/secret.key [...] ``` ``` $ ./restore-keystore.ts $HOME/.local/share/Daedalus/mainnet/Secrets/secret.key [...] ``` * [DDW-563] Import TS * [DDW-563] Integrate restore-keystore lib * [DDW-563] Adds CHANGELOG, improves import * [DDW-563] Improve import * [DDW-563] Move restore-keystore.js into Release * [DDW-563] Force add dist dir to GH * [DDW-563] Remove unnecessary files * [DDW-563] Styling fixes * [DDW-563] Updates text copy Co-authored-by: Nikola Glumac <niglumac@gmail.com>
1 parent 31ec48d commit 7cb5032

File tree

12 files changed

+102
-39
lines changed

12 files changed

+102
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55

66
### Features
77

8+
- Re-enabled "Wallet import" feature ([PR 2308](https://github.com/input-output-hk/daedalus/pull/2308))
89
- Implemented Voting Centar ([PR 2315](https://github.com/input-output-hk/daedalus/pull/2315), [PR 2353](https://github.com/input-output-hk/daedalus/pull/2353), [PR 2354](https://github.com/input-output-hk/daedalus/pull/2354))
910
- Implemented transaction metadata display ([PR 2338](https://github.com/input-output-hk/daedalus/pull/2338))
1011
- Displayed fee and deposit info in transaction details and in the delegation wizard ([PR 2339](https://github.com/input-output-hk/daedalus/pull/2339))

source/main/cardano/utils.js

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { spawnSync } from 'child_process';
66
import { logger } from '../utils/logging';
77
import { getTranslation } from '../utils/getTranslation';
88
import ensureDirectoryExists from '../utils/ensureDirectoryExists';
9+
import { decodeKeystore } from '../utils/restoreKeystore';
910
import type { LauncherConfig } from '../config';
1011
import type { ExportWalletsMainResponse } from '../../common/ipc/api';
1112
import type {
@@ -19,7 +20,6 @@ import {
1920
CardanoProcessNameOptions,
2021
CardanoNodeImplementationOptions,
2122
NetworkNameOptions,
22-
TESTNET_MAGIC,
2323
} from '../../common/types/cardano-node.types';
2424

2525
export type Process = {
@@ -176,7 +176,6 @@ export const exportWallets = async (
176176
locale: string
177177
): Promise<ExportWalletsMainResponse> => {
178178
const {
179-
exportWalletsBin,
180179
legacySecretKey,
181180
legacyWalletDB,
182181
stateDir,
@@ -186,7 +185,6 @@ export const exportWallets = async (
186185

187186
logger.info('ipcMain: Starting wallets export...', {
188187
exportSourcePath,
189-
exportWalletsBin,
190188
legacySecretKey,
191189
legacyWalletDB,
192190
stateDir,
@@ -226,41 +224,38 @@ export const exportWallets = async (
226224
}
227225
}
228226

229-
// Export tool flags
230-
const exportWalletsBinFlags = [];
231-
232-
// Cluster flags
233-
if (cluster === 'testnet') {
234-
exportWalletsBinFlags.push('--testnet', TESTNET_MAGIC.toString());
235-
} else {
236-
exportWalletsBinFlags.push('--mainnet');
237-
}
238-
239-
// Secret key flags
240-
exportWalletsBinFlags.push('--keyfile', legacySecretKeyPath);
241-
242-
// Wallet DB flags
243227
const legacyWalletDBPathExists = await fs.pathExists(
244228
`${legacyWalletDBPath}-acid`
245229
);
246-
if (legacyWalletDBPathExists) {
247-
exportWalletsBinFlags.push('--wallet-db-path', legacyWalletDBPath);
248-
}
249230

250231
logger.info('ipcMain: Exporting wallets...', {
251-
exportWalletsBin,
252-
exportWalletsBinFlags,
232+
legacySecretKeyPath,
233+
legacyWalletDBPath,
234+
legacyWalletDBPathExists,
253235
});
254236

255-
const { stdout, stderr } = spawnSync(exportWalletsBin, exportWalletsBinFlags);
256-
const wallets = JSON.parse(stdout.toString() || '[]');
257-
const errors = stderr.toString();
237+
let wallets = [];
238+
let errors = '';
239+
try {
240+
const legacySecretKeyFile = fs.readFileSync(legacySecretKeyPath);
241+
// $FlowFixMe
242+
const rawWallets = await decodeKeystore(legacySecretKeyFile);
243+
wallets = rawWallets.map((w) => ({
244+
name: null,
245+
id: w.walletId,
246+
isEmptyPassphrase: w.isEmptyPassphrase,
247+
passphrase_hash: w.passphraseHash.toString('hex'),
248+
encrypted_root_private_key: w.encryptedPayload.toString('hex'),
249+
}));
250+
} catch (error) {
251+
errors = error.toString();
252+
}
258253

259254
logger.info(`ipcMain: Exported ${wallets.length} wallets`, {
260255
walletsData: wallets.map((w) => ({
261256
name: w.name,
262257
id: w.id,
263-
hasPassword: w.is_passphrase_empty,
258+
hasPassword: !w.isEmptyPassphrase,
264259
})),
265260
errors,
266261
});

source/main/config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export type LauncherConfig = {
6969
configPath: string,
7070
syncTolerance: string,
7171
cliBin: string,
72-
exportWalletsBin: string,
7372
legacyStateDir: string,
7473
legacySecretKey: string,
7574
legacyWalletDB: string,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// @flow
2+
import * as cbor from 'cbor';
3+
import * as blake2b from 'blake2b';
4+
import * as crypto from 'crypto';
5+
6+
export type EncryptedSecretKeys = Array<EncryptedSecretKey>;
7+
8+
export type EncryptedSecretKey = {
9+
encryptedPayload: Buffer,
10+
passphraseHash: Buffer,
11+
isEmptyPassphrase: boolean,
12+
walletId: WalletId,
13+
};
14+
15+
export type WalletId = string;
16+
17+
export const decodeKeystore = async (
18+
bytes: Buffer
19+
): Promise<EncryptedSecretKeys> => {
20+
return cbor
21+
.decodeAll(bytes)
22+
.then((obj) => obj[0][2].map(toEncryptedSecretKey));
23+
};
24+
25+
const toEncryptedSecretKey = ([encryptedPayload, passphraseHash]: [
26+
Buffer,
27+
Buffer
28+
]): EncryptedSecretKey => {
29+
const isEmptyPassphrase = $isEmptyPassphrase(passphraseHash);
30+
return {
31+
walletId: mkWalletId(encryptedPayload),
32+
encryptedPayload,
33+
passphraseHash,
34+
isEmptyPassphrase,
35+
};
36+
};
37+
38+
const mkWalletId = (xprv: Buffer): WalletId => {
39+
const xpub = xprv.slice(64);
40+
return blake2b(20).update(xpub).digest('hex');
41+
};
42+
43+
const $isEmptyPassphrase = (pwd: Buffer): boolean => {
44+
const cborEmptyBytes = Buffer.from('40', 'hex');
45+
const [logN, r, p, salt, hashA] = pwd.toString('utf8').split('|');
46+
const opts = { N: 2 ** Number(logN), r: Number(r), p: Number(p) };
47+
// $FlowFixMe
48+
const hashB = crypto
49+
.scryptSync(cborEmptyBytes, Buffer.from(salt, 'base64'), 32, opts)
50+
.toString('base64');
51+
return hashA === hashB;
52+
};

source/renderer/app/components/wallet/WalletAdd.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ export default class WalletAdd extends Component<Props> {
191191
label={intl.formatMessage(messages.importLabel)}
192192
description={intl.formatMessage(messages.importDescription)}
193193
isDisabled={
194-
true || // This feature is currently unavailable as export tool is disabled
195194
isMaxNumberOfWalletsReached ||
196195
(isProduction && !(isMainnet || isTestnet))
197196
}

source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export default class WalletSelectImportDialog extends Component<Props> {
149149
walletStatus = alreadyExistsStatus;
150150
} else if (wallet.import.status === WalletImportStatuses.COMPLETED) {
151151
walletStatus = walletImportedStatus;
152-
} else if (wallet.is_passphrase_empty) {
152+
} else if (wallet.isEmptyPassphrase) {
153153
walletStatus = noPasswordStatus;
154154
} else {
155155
walletStatus = hasPasswordStatus;

source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@
184184
display: flex;
185185
padding-right: 20px;
186186

187+
:global {
188+
.InlineEditingSmallInput_component {
189+
margin-bottom: 0 !important;
190+
}
191+
}
192+
187193
.walletsInputFieldInner {
188194
input {
189195
color: var(--theme-wallet-import-button-text-color);
@@ -258,6 +264,14 @@
258264
margin-top: -3px;
259265
}
260266
}
267+
268+
.LoadingSpinner_component {
269+
margin: 0 !important;
270+
271+
.LoadingSpinner_icon svg path {
272+
fill: var(--theme-wallet-import-button-text-color) !important;
273+
}
274+
}
261275
}
262276

263277
.walletsStatusIconCheckmark {

source/renderer/app/config/walletsConfig.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ export const RECOVERY_PHRASE_WORD_COUNT_OPTIONS = {
3434
export const WALLET_PUBLIC_KEY_NOTIFICATION_SEGMENT_LENGTH = 15;
3535
export const WALLET_PUBLIC_KEY_SHARING_ENABLED = false;
3636

37+
// Automatic wallet migration from pre Daedalus 1.0.0 versions has been disabled
38+
export const IS_AUTOMATIC_WALLET_MIGRATION_ENABLED = false;
39+
3740
// Byron wallet migration has been temporarily disabled due to missing Api support after Mary HF
3841
export const IS_BYRON_WALLET_MIGRATION_ENABLED = false;

source/renderer/app/i18n/locales/en-US.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@
767767
"wallet.hardware.deviceStatus.wrong_firmware.link.label": "Firmware update instructions",
768768
"wallet.hardware.deviceStatus.wrong_firmware.link.url": "https://trezor.io/start/",
769769
"wallet.import.file.dialog.buttonLabel": "Import wallets",
770-
"wallet.import.file.dialog.description": "<p>This feature enables you to import wallets from the previous version of Daedalus, from the Daedalus state directory, or from a ‘secret.key’ file.</p> <p>It can be used to import wallets quickly without entering the wallet recovery phrase for each wallet, or to import wallets for which you have lost your wallet recovery phrase.</p> <p>After importing a wallet for which you have lost your wallet recovery phrase, please create a new wallet and transfer all funds from the old wallet to the new wallet. <strong>Keep the wallet recovery phrase for your new wallet secure.</strong></p>",
770+
"wallet.import.file.dialog.description": "<p>This feature enables you to import wallets from ‘secret.key’ files of old versions of Daedalus (Daedalus version 0.15.1 and previous). Importing wallets from state directories of version Daedalus 1.0 onwards is currently not supported.</p> <p>It can be used to import wallets quickly without entering the wallet recovery phrase for each wallet, or to import wallets for which you have lost your wallet recovery phrase.</p> <p>After importing a wallet for which you have lost your wallet recovery phrase, please create a new wallet and transfer all funds from the old wallet to the new wallet. <strong>Keep the wallet recovery phrase for your new wallet secure.</strong></p>",
771771
"wallet.import.file.dialog.importFromLabel": "Import from:",
772772
"wallet.import.file.dialog.linkLabel": "Learn more",
773773
"wallet.import.file.dialog.linkUrl": "https://iohk.zendesk.com/hc/en-us/articles/900000623463",

source/renderer/app/i18n/locales/ja-JP.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@
767767
"wallet.hardware.deviceStatus.wrong_firmware.link.label": "ファームウェア更新ガイド",
768768
"wallet.hardware.deviceStatus.wrong_firmware.link.url": "https://trezor.io/start/",
769769
"wallet.import.file.dialog.buttonLabel": "ウォレットをインポートする",
770-
"wallet.import.file.dialog.description": "<p>この機能により、Daedalusの旧バージョン、Daedalusステータスディレクトリー、またはsecret.keyファイルからウォレットをインポートすることができます。</p> <p>各ウォレットの復元フレーズを入力せずに素早くウォレットをインポートできるほか、復元フレーズを紛失したウォレットのインポートも可能です。</p> <p>復元フレーズを紛失したウォレットをインポートした場合は、インポート後に新規ウォレットを作成してすべての資金を旧ウォレットから移し、<strong>新しいウォレットの復元フレーズを安全な場所に保管してください。</strong></p>",
770+
"wallet.import.file.dialog.description": "<p>この機能により、Daedalus旧バージョン(Daedalus 0.15.1以前)の「secret.key」からウォレットをインポートすることができます。Daedalus 1.0以降のステータスディレクトリーからのウォレットインポートは現在サポートされていません。</p> <p>各ウォレットの復元フレーズを入力せずに素早くウォレットをインポートできるほか、復元フレーズを紛失したウォレットのインポートも可能です。</p> <p>復元フレーズを紛失したウォレットをインポートした場合は、インポート後に新規ウォレットを作成してすべての資金を旧ウォレットから移し、<strong>新しいウォレットの復元フレーズを安全な場所に保管してください。</strong></p>",
771771
"wallet.import.file.dialog.importFromLabel": "インポート元:",
772772
"wallet.import.file.dialog.linkLabel": "もっと知る",
773773
"wallet.import.file.dialog.linkUrl": "https://iohk.zendesk.com/hc/ja/articles/900000623463",

0 commit comments

Comments
 (0)