diff --git a/CHANGELOG.md b/CHANGELOG.md index 2075d7db24..39dec1e317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changelog ### Features +- Re-enabled "Wallet import" feature ([PR 2308](https://github.com/input-output-hk/daedalus/pull/2308)) - 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)) - Implemented transaction metadata display ([PR 2338](https://github.com/input-output-hk/daedalus/pull/2338)) - Displayed fee and deposit info in transaction details and in the delegation wizard ([PR 2339](https://github.com/input-output-hk/daedalus/pull/2339)) diff --git a/source/main/cardano/utils.js b/source/main/cardano/utils.js index 4194f8e851..e983a51793 100644 --- a/source/main/cardano/utils.js +++ b/source/main/cardano/utils.js @@ -6,6 +6,7 @@ import { spawnSync } from 'child_process'; import { logger } from '../utils/logging'; import { getTranslation } from '../utils/getTranslation'; import ensureDirectoryExists from '../utils/ensureDirectoryExists'; +import { decodeKeystore } from '../utils/restoreKeystore'; import type { LauncherConfig } from '../config'; import type { ExportWalletsMainResponse } from '../../common/ipc/api'; import type { @@ -19,7 +20,6 @@ import { CardanoProcessNameOptions, CardanoNodeImplementationOptions, NetworkNameOptions, - TESTNET_MAGIC, } from '../../common/types/cardano-node.types'; export type Process = { @@ -176,7 +176,6 @@ export const exportWallets = async ( locale: string ): Promise => { const { - exportWalletsBin, legacySecretKey, legacyWalletDB, stateDir, @@ -186,7 +185,6 @@ export const exportWallets = async ( logger.info('ipcMain: Starting wallets export...', { exportSourcePath, - exportWalletsBin, legacySecretKey, legacyWalletDB, stateDir, @@ -226,41 +224,38 @@ export const exportWallets = async ( } } - // Export tool flags - const exportWalletsBinFlags = []; - - // Cluster flags - if (cluster === 'testnet') { - exportWalletsBinFlags.push('--testnet', TESTNET_MAGIC.toString()); - } else { - exportWalletsBinFlags.push('--mainnet'); - } - - // Secret key flags - exportWalletsBinFlags.push('--keyfile', legacySecretKeyPath); - - // Wallet DB flags const legacyWalletDBPathExists = await fs.pathExists( `${legacyWalletDBPath}-acid` ); - if (legacyWalletDBPathExists) { - exportWalletsBinFlags.push('--wallet-db-path', legacyWalletDBPath); - } logger.info('ipcMain: Exporting wallets...', { - exportWalletsBin, - exportWalletsBinFlags, + legacySecretKeyPath, + legacyWalletDBPath, + legacyWalletDBPathExists, }); - const { stdout, stderr } = spawnSync(exportWalletsBin, exportWalletsBinFlags); - const wallets = JSON.parse(stdout.toString() || '[]'); - const errors = stderr.toString(); + let wallets = []; + let errors = ''; + try { + const legacySecretKeyFile = fs.readFileSync(legacySecretKeyPath); + // $FlowFixMe + const rawWallets = await decodeKeystore(legacySecretKeyFile); + wallets = rawWallets.map((w) => ({ + name: null, + id: w.walletId, + isEmptyPassphrase: w.isEmptyPassphrase, + passphrase_hash: w.passphraseHash.toString('hex'), + encrypted_root_private_key: w.encryptedPayload.toString('hex'), + })); + } catch (error) { + errors = error.toString(); + } logger.info(`ipcMain: Exported ${wallets.length} wallets`, { walletsData: wallets.map((w) => ({ name: w.name, id: w.id, - hasPassword: w.is_passphrase_empty, + hasPassword: !w.isEmptyPassphrase, })), errors, }); diff --git a/source/main/config.js b/source/main/config.js index e5204776e4..c4b4577bce 100644 --- a/source/main/config.js +++ b/source/main/config.js @@ -69,7 +69,6 @@ export type LauncherConfig = { configPath: string, syncTolerance: string, cliBin: string, - exportWalletsBin: string, legacyStateDir: string, legacySecretKey: string, legacyWalletDB: string, diff --git a/source/main/utils/restoreKeystore.js b/source/main/utils/restoreKeystore.js new file mode 100644 index 0000000000..0d8461a35b --- /dev/null +++ b/source/main/utils/restoreKeystore.js @@ -0,0 +1,52 @@ +// @flow +import * as cbor from 'cbor'; +import * as blake2b from 'blake2b'; +import * as crypto from 'crypto'; + +export type EncryptedSecretKeys = Array; + +export type EncryptedSecretKey = { + encryptedPayload: Buffer, + passphraseHash: Buffer, + isEmptyPassphrase: boolean, + walletId: WalletId, +}; + +export type WalletId = string; + +export const decodeKeystore = async ( + bytes: Buffer +): Promise => { + return cbor + .decodeAll(bytes) + .then((obj) => obj[0][2].map(toEncryptedSecretKey)); +}; + +const toEncryptedSecretKey = ([encryptedPayload, passphraseHash]: [ + Buffer, + Buffer +]): EncryptedSecretKey => { + const isEmptyPassphrase = $isEmptyPassphrase(passphraseHash); + return { + walletId: mkWalletId(encryptedPayload), + encryptedPayload, + passphraseHash, + isEmptyPassphrase, + }; +}; + +const mkWalletId = (xprv: Buffer): WalletId => { + const xpub = xprv.slice(64); + return blake2b(20).update(xpub).digest('hex'); +}; + +const $isEmptyPassphrase = (pwd: Buffer): boolean => { + const cborEmptyBytes = Buffer.from('40', 'hex'); + const [logN, r, p, salt, hashA] = pwd.toString('utf8').split('|'); + const opts = { N: 2 ** Number(logN), r: Number(r), p: Number(p) }; + // $FlowFixMe + const hashB = crypto + .scryptSync(cborEmptyBytes, Buffer.from(salt, 'base64'), 32, opts) + .toString('base64'); + return hashA === hashB; +}; diff --git a/source/renderer/app/components/wallet/WalletAdd.js b/source/renderer/app/components/wallet/WalletAdd.js index 19138248b3..f169c327e8 100644 --- a/source/renderer/app/components/wallet/WalletAdd.js +++ b/source/renderer/app/components/wallet/WalletAdd.js @@ -191,7 +191,6 @@ export default class WalletAdd extends Component { label={intl.formatMessage(messages.importLabel)} description={intl.formatMessage(messages.importDescription)} isDisabled={ - true || // This feature is currently unavailable as export tool is disabled isMaxNumberOfWalletsReached || (isProduction && !(isMainnet || isTestnet)) } diff --git a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js index 4fd86244be..9c11f2004e 100644 --- a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js +++ b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js @@ -149,7 +149,7 @@ export default class WalletSelectImportDialog extends Component { walletStatus = alreadyExistsStatus; } else if (wallet.import.status === WalletImportStatuses.COMPLETED) { walletStatus = walletImportedStatus; - } else if (wallet.is_passphrase_empty) { + } else if (wallet.isEmptyPassphrase) { walletStatus = noPasswordStatus; } else { walletStatus = hasPasswordStatus; diff --git a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss index 3c330e782b..f4838aead9 100644 --- a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss +++ b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss @@ -184,6 +184,12 @@ display: flex; padding-right: 20px; + :global { + .InlineEditingSmallInput_component { + margin-bottom: 0 !important; + } + } + .walletsInputFieldInner { input { color: var(--theme-wallet-import-button-text-color); @@ -258,6 +264,14 @@ margin-top: -3px; } } + + .LoadingSpinner_component { + margin: 0 !important; + + .LoadingSpinner_icon svg path { + fill: var(--theme-wallet-import-button-text-color) !important; + } + } } .walletsStatusIconCheckmark { diff --git a/source/renderer/app/config/walletsConfig.js b/source/renderer/app/config/walletsConfig.js index 30fb75fe6b..5291f1eb17 100644 --- a/source/renderer/app/config/walletsConfig.js +++ b/source/renderer/app/config/walletsConfig.js @@ -34,5 +34,8 @@ export const RECOVERY_PHRASE_WORD_COUNT_OPTIONS = { export const WALLET_PUBLIC_KEY_NOTIFICATION_SEGMENT_LENGTH = 15; export const WALLET_PUBLIC_KEY_SHARING_ENABLED = false; +// Automatic wallet migration from pre Daedalus 1.0.0 versions has been disabled +export const IS_AUTOMATIC_WALLET_MIGRATION_ENABLED = false; + // Byron wallet migration has been temporarily disabled due to missing Api support after Mary HF export const IS_BYRON_WALLET_MIGRATION_ENABLED = false; diff --git a/source/renderer/app/i18n/locales/en-US.json b/source/renderer/app/i18n/locales/en-US.json index 350c687a90..49281e2f61 100755 --- a/source/renderer/app/i18n/locales/en-US.json +++ b/source/renderer/app/i18n/locales/en-US.json @@ -767,7 +767,7 @@ "wallet.hardware.deviceStatus.wrong_firmware.link.label": "Firmware update instructions", "wallet.hardware.deviceStatus.wrong_firmware.link.url": "https://trezor.io/start/", "wallet.import.file.dialog.buttonLabel": "Import wallets", - "wallet.import.file.dialog.description": "

This feature enables you to import wallets from the previous version of Daedalus, from the Daedalus state directory, or from a ‘secret.key’ file.

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.

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. Keep the wallet recovery phrase for your new wallet secure.

", + "wallet.import.file.dialog.description": "

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.

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.

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. Keep the wallet recovery phrase for your new wallet secure.

", "wallet.import.file.dialog.importFromLabel": "Import from:", "wallet.import.file.dialog.linkLabel": "Learn more", "wallet.import.file.dialog.linkUrl": "https://iohk.zendesk.com/hc/en-us/articles/900000623463", diff --git a/source/renderer/app/i18n/locales/ja-JP.json b/source/renderer/app/i18n/locales/ja-JP.json index afb6700c10..de8f4d426f 100755 --- a/source/renderer/app/i18n/locales/ja-JP.json +++ b/source/renderer/app/i18n/locales/ja-JP.json @@ -767,7 +767,7 @@ "wallet.hardware.deviceStatus.wrong_firmware.link.label": "ファームウェア更新ガイド", "wallet.hardware.deviceStatus.wrong_firmware.link.url": "https://trezor.io/start/", "wallet.import.file.dialog.buttonLabel": "ウォレットをインポートする", - "wallet.import.file.dialog.description": "

この機能により、Daedalusの旧バージョン、Daedalusステータスディレクトリー、またはsecret.keyファイルからウォレットをインポートすることができます。

各ウォレットの復元フレーズを入力せずに素早くウォレットをインポートできるほか、復元フレーズを紛失したウォレットのインポートも可能です。

復元フレーズを紛失したウォレットをインポートした場合は、インポート後に新規ウォレットを作成してすべての資金を旧ウォレットから移し、新しいウォレットの復元フレーズを安全な場所に保管してください。

", + "wallet.import.file.dialog.description": "

この機能により、Daedalus旧バージョン(Daedalus 0.15.1以前)の「secret.key」からウォレットをインポートすることができます。Daedalus 1.0以降のステータスディレクトリーからのウォレットインポートは現在サポートされていません。

各ウォレットの復元フレーズを入力せずに素早くウォレットをインポートできるほか、復元フレーズを紛失したウォレットのインポートも可能です。

復元フレーズを紛失したウォレットをインポートした場合は、インポート後に新規ウォレットを作成してすべての資金を旧ウォレットから移し、新しいウォレットの復元フレーズを安全な場所に保管してください。

", "wallet.import.file.dialog.importFromLabel": "インポート元:", "wallet.import.file.dialog.linkLabel": "もっと知る", "wallet.import.file.dialog.linkUrl": "https://iohk.zendesk.com/hc/ja/articles/900000623463", diff --git a/source/renderer/app/stores/WalletMigrationStore.js b/source/renderer/app/stores/WalletMigrationStore.js index f8e49a1eb0..5f06be05ee 100644 --- a/source/renderer/app/stores/WalletMigrationStore.js +++ b/source/renderer/app/stores/WalletMigrationStore.js @@ -28,6 +28,7 @@ import { ImportFromOptions, } from '../types/walletExportTypes'; import { IMPORT_WALLET_STEPS } from '../config/walletRestoreConfig'; +import { IS_AUTOMATIC_WALLET_MIGRATION_ENABLED } from '../config/walletsConfig'; import type { ImportWalletStep } from '../types/walletRestoreTypes'; export type WalletMigrationStatus = @@ -245,7 +246,7 @@ export default class WalletMigrationStore extends Store { : WalletImportStatuses.UNSTARTED; return { ...wallet, hasName, import: { status, error: null } }; }), - ['hasName', 'id', 'name', 'is_passphrase_empty'], + ['hasName', 'id', 'name', 'isEmptyPassphrase'], ['desc', 'asc', 'asc', 'asc'] ); @@ -286,7 +287,7 @@ export default class WalletMigrationStore extends Store { { id: wallet.id, name: wallet.name, - hasPassword: wallet.is_passphrase_empty, + hasPassword: !wallet.isEmptyPassphrase, } ); return this._restoreWallet(wallet); @@ -333,7 +334,7 @@ export default class WalletMigrationStore extends Store { }); } catch (error) { runInAction('update restorationErrors', () => { - const { name, is_passphrase_empty: hasPassword } = exportedWallet; + const { name, isEmptyPassphrase } = exportedWallet; this._updateWalletImportStatus( index, WalletImportStatuses.ERRORED, @@ -341,7 +342,7 @@ export default class WalletMigrationStore extends Store { ); this.restorationErrors.push({ error, - wallet: { id, name, hasPassword }, + wallet: { id, name, hasPassword: !isEmptyPassphrase }, }); }); } @@ -381,8 +382,7 @@ export default class WalletMigrationStore extends Store { }; @action _startMigration = async () => { - // eslint-disable-next-line - if (true) return; // This feature is currently unavailable as export tool is disabled + if (!IS_AUTOMATIC_WALLET_MIGRATION_ENABLED) return; const { isMainnet, isTestnet, isTest } = this.environment; if (isMainnet || isTestnet || (isTest && this.isTestMigrationEnabled)) { @@ -514,7 +514,7 @@ export default class WalletMigrationStore extends Store { return this.exportedWallets.map((wallet) => ({ id: wallet.id, name: wallet.name, - hasPassword: wallet.is_passphrase_empty, + hasPassword: !wallet.isEmptyPassphrase, import: wallet.import, })); } diff --git a/source/renderer/app/types/walletExportTypes.js b/source/renderer/app/types/walletExportTypes.js index fc94449019..6163455184 100644 --- a/source/renderer/app/types/walletExportTypes.js +++ b/source/renderer/app/types/walletExportTypes.js @@ -33,7 +33,7 @@ export type ExportedByronWallet = { name: ?string, id: string, passphrase_hash: string, - is_passphrase_empty: boolean, + isEmptyPassphrase: boolean, // Daedalus derived wallet props hasName: boolean,