diff --git a/.changelog/1771.internal.md b/.changelog/1771.internal.md new file mode 100644 index 0000000000..429dc13e60 --- /dev/null +++ b/.changelog/1771.internal.md @@ -0,0 +1 @@ +Add utils for migration from V0 encrypted state to RootState diff --git a/package.json b/package.json index da8ccb9eff..e3e4f71f93 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@capacitor/core": "5.0.4", "@ethereumjs/util": "9.0.0", "@ledgerhq/hw-transport-webusb": "6.27.20", + "@metamask/browser-passworder": "=3.0.0", "@metamask/jazzicon": "2.0.0", "@oasisprotocol/client": "0.1.1-alpha.2", "@oasisprotocol/client-rt": "0.2.1-alpha.2", diff --git a/renovate.json b/renovate.json index 74670a2d1b..acf8d9b8c1 100644 --- a/renovate.json +++ b/renovate.json @@ -33,6 +33,11 @@ { "groupName": "CI github-actions", "matchManagers": ["github-actions"] + }, + { + "description": "Packages needed for migration from V0 extension should stay at the pinned version (https://github.com/oasisprotocol/oasis-wallet-ext/blob/master/package.json#L14)", + "matchPackageNames": ["@metamask/browser-passworder"], + "enabled": false } ], "rangeStrategy": "bump" diff --git a/src/app/components/LanguageSelect/index.tsx b/src/app/components/LanguageSelect/index.tsx index 38a0524996..031d937eaa 100644 --- a/src/app/components/LanguageSelect/index.tsx +++ b/src/app/components/LanguageSelect/index.tsx @@ -4,14 +4,12 @@ import { Menu } from 'grommet/es6/components/Menu' import { Text } from 'grommet/es6/components/Text' import { Language } from 'styles/theme/icons/language/Language' import { SelectWithIcon } from '../SelectWithIcon' -import { languageLabels, translationsJson } from '../../../locales/i18n' +import { languageLabels, LanguageKey } from '../../../locales/i18n' -const languageOptions: { value: keyof typeof translationsJson; label: string }[] = languageLabels.map( - lang => ({ - value: lang[0], - label: lang[1], - }), -) +const languageOptions: { value: LanguageKey; label: string }[] = languageLabels.map(lang => ({ + value: lang[0], + label: lang[1], +})) export const LanguageSelect = () => { const { t, i18n } = useTranslation() diff --git a/src/locales/i18n.ts b/src/locales/i18n.ts index 478da09ec9..058e7c51f2 100644 --- a/src/locales/i18n.ts +++ b/src/locales/i18n.ts @@ -6,6 +6,7 @@ import en from './en/translation.json' import fr from './fr/translation.json' import sl from './sl/translation.json' import tr from './tr/translation.json' +import zh_CN from './zh_CN/translation.json' export const translationsJson = { en: { @@ -20,13 +21,20 @@ export const translationsJson = { tr: { translation: tr, }, + zh_CN: { + translation: zh_CN, + }, } -export const languageLabels: [keyof typeof translationsJson, string][] = [ +export type LanguageKey = keyof typeof translationsJson + +export const languageLabels: [LanguageKey, string][] = [ ['en', 'English'], ['fr', 'Français'], ['sl', 'Slovenščina'], ['tr', 'Türkçe'], + // TODO: enable when translated + // ['zh_CN', '中文'], ] export const i18n = i18next diff --git a/src/locales/zh_CN/translation.json b/src/locales/zh_CN/translation.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/locales/zh_CN/translation.json @@ -0,0 +1 @@ +{} diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts index 2332c13178..2fd975939b 100644 --- a/src/utils/__fixtures__/test-inputs.ts +++ b/src/utils/__fixtures__/test-inputs.ts @@ -2,6 +2,7 @@ import type { ImportAccountsStep } from 'app/state/importaccounts/types' import type { TransactionFormSteps } from 'app/state/paratimes/types' import type { WalletType } from 'app/state/wallet/types' import type { RootState } from 'types' +import type { EncryptedData, StringifiedType, WalletExtensionV0State } from '../walletExtensionV0' export const mnemonicAddress0 = 'oasis1qqca0gplrfn63ljg9c833te7em36lkz0cv8djffh' export const mnemonicAddress0Pretty = 'oasis1 qqca 0gpl rfn6 3ljg 9c83 3te7 em36 lkz0 cv8d jffh' @@ -23,7 +24,7 @@ export const privateKey2 = ` export const password = 'Abcd1234&' export const wrongPassword = 'wrongPassword1&' -function typedStringify(obj: T): string & { stringifiedType: T } { +function typedStringify(obj: T): StringifiedType { return JSON.stringify(obj) as any } @@ -135,3 +136,73 @@ export const privateKeyUnlockedState = { enteredWrongPassword: false, }, } satisfies RootState + +export const walletExtensionV0PersistedState = { + chromeStorageLocal: { + keyringData: JSON.stringify({ + data: '6wVltZe5h8jKU/Lp1Xdq8CnTuzmg3+Pj3n3Mcac/wcbkGWdUVTpA8I8YNY9wyoGrS2wy2h/ITSSoLK/FdATeIZzJAFSpGeEkkdhSvmpjp6Azh2z9xVEhW+jbnRPqR9d45reBkVVbas8Mohp8J2zyj7lNrd3yXhcwbAdmUdtCKVZa3RXmVHx9t8m33xb1kjju9KteQLMEjnncpRNCJq48i+z3AletozbjAiZjRjq9MJhmy1NViR5LcAyQOjGQzuEIPpWikPHxGrDHySWN5mSQjwoSKsHx1M8L9u1ZyYJXADE43QTQtHphSsNzAV3i88GB1LbRez6QqI1ThQXV/NQYwNnWS6INgS5AsmbUBSz2f6uYQoYbzeM15QcbwIUQayMK44Q6C9p+ZzZuf1FKcVAn2LHdhfgUF4dUbOkqe5Irwr05pK6zDznZC9+Zh53SOCsygJV2Yt8CnTStb80PhaxjJRdyB1zwae2KjUliK9G4Pna2R4XI2c4MtB4gcR1tbTC9NWQ5l3IQgLqXwD1nq1pvh1qgCccH5oUYUJoRaeYoew/h2Xmo1rFCJ0R6RHW0cmxLbnWreLf/bBbQfou4EKkF//67IdngkddngYwnraW/Bnn44iNPEa8lDhkGYhiAs2KEB5EfEG/awZQ6DkmwFeeh0H0MuYQUl2MGOSn+EQShjeW1diipB5xDReYUSBfSC09ZB83oTcETZsc3y5Pn/Sj0Krg3mill69yaV3WgaIHF1tgJoarXqo1IIwDI5lAqWhrGXNJukZz7zDY4f9mwFrRYXDxBeDuUMlvWkEYc2rlv9aUoqeqbzchrKP0FuXndN1T9hr+uM4tL80GpGTelh5iGceCEQgACSWWWEmmnX8Gr6iAsOx7FeZktvy4LGM+E9uQTPb6gEMOFRS+hEWnbzFblaWA43sDA6EF1rmwsWaPHyGLnutC527y8BlULn5PE04jPbIbfqyzATjuuu9eCBJkduvGR6btI2DiXXZwzfq+bmqQHparyX5XRs9dxWKNTRgkV5oqIaZ86Pk/TvM/bwIvV9G4F3TFEp3OhZvfCx1Ta8LeNULksZNbi9QX9uM83LRffV+D9u7fT3yMVJGEA3xzxq3kQAKQschBvgQdmayJG9pl0ZvEDx0PsAiwfUC9x+LwZu/cTg9X4dAuk8MThL4MtJzyly1dsQWxSoZjG+f+EHdyn9zrCUXWkt6eRUJfDp7Pgfqv2LNIjQZDIVPiRFih507CjTGvV8dpF6yjDYDnwQCilvvwZVqJrrn8Ykyyfd2cbUpg3YDqDlXZ+BU4UJ8pvAbcMLGT2Izyr5XJPB8lYafQJRXg/U3HFxSj8QYLeK/qWPGUp9RJlMcCzTEGPoGK/Md0yfA8OKly7MSbhjK1RsJomQD9ig9TjHTkaXId+wTYF87ZT1yRTxKM/yCRh6KkClHGje9WzvOnqn7nu4QvM8SdIvtEkyyHnPfj5HxPlMgKysyrOfWQifB3Gv4LZX/yxebK3jb3pJYHqtlS8uajMXkS0Q/5GNuLu7flDVqoqqBMJrDobGQrzu0XuhcMBR+xl+a+M3MrlCEBfdoS5wuz9CSlxtDD9zy2905MtWXkoAsqrOucWtN58D4PK+vffwD+pn1ut6Y5tJgaPhxLW2fpzcezSUDN0IbuyUnUp2PIMu+nU6id7onloX9R/Lm/ZT4UMJgP7nT/9rYG5jNjNc4H29tBj4CeeTYauTkXEdk00cunvphD8PawZTV5TjNhUCxq+NtAzvJvrbG74Yn5ErVVok15ZLOfMrTPaIRoqxHqOfJGTMLbN+fpg5N2GnlyZfo1/dAG0QlTTJKo9xMTx1QhjqNYCKX57OEjTJ3Rc2r3TWkVPXwgyBTPjSh4C9PSEhI2GhAnLp8UYzcvO4l9g2luK9FnE4RTP5qyoXQjYZUfYXlE7p96et9yVSPiY7gwKR+W91BA5wxQqQcK8ceHmMzwfVt8BE82rulBTM3sWm6YwkALm/Rk9Dw5zUFAl7KnEF4zmioq5bYeCBqEQyMDUwbmdFQADR90pHke3Ql8OZJSwvoeMhYO1vm3mHMm8bw0VcytScsjdzX7kRABmMxPdQGYPt+rL82i5OOXi10x0Zbc1QZo8Cv8A9onTu/OY9j8Uc/4n6/oB8mM0WHL3PTWuGNfxHBc3WfqjSy39lIicviO2TmRDhOg9Tl5TQIiBGUhd+/VBiUnMuJP3gdsimj3z8PB7nQIX9kzCmWanKbEZx9rojS7coQRO4eo/CVz4dGuUZG1EfFmdh6XsIQIp6sxOcaxCiYlhSA/rMBa10oUN0Pe4kWuHQUU0jeARIQwWmhKuU3WKBnSnneKdF61RD9AqD6KOYVprUtP1m0swGfFu/u6aSu0oU3E213WuYc94/er0YtKyg6nzawXqVY9DmjvbvlbgdnaBOmDspdt52J0DfYdEoLuRVCy3IyupfjSbGGllQRD3emkgsTgMZc4wX47p3MY4Zx1dD6ifAYuB5sCAYzGSzfrRaW16BYodDDe4gCiXbvqcZGKouAd11LmpxpRzzu7GNUc3O6/AKBl8tpze2g2BdjsKP+Up0kgmGWo7qrLONyMka996c+3d2lfkjNOrrEbSDkVghxWwCeMiq5LO1jS6rXLDt8VOzdMSCq6k+K4mGMF8wMvIAUEr5nOFkl5XV0s7SehbwbvZ1f6H2ti1w2b6PdIqx+XhDYySXbl+C26GOz3XYt4G/DZyXus7jaAL1ca1MYkeV+4BJDKsS0+unv9TczC9PHUBhFdUg1ttaO7725B4dIg780UO/cwEXZ/ECKBNOe2LQpp0nxrNwZDEnVrzCdPpJs1IrUK876XTAKUhn5vGoKOBPDMxxCYuBH1LSp3doQr1PdgsY83YL5HEgwECNPOvzHuRYTaw/E3TlK/mTNaUk0Xu3NGm957NX6LWb9qP5Im6M1R0ypazhSqDiH+KUIeXiHG1Lkxo6UafepNIt7ushDathY9Ef8mxLfzZMHiPN5FRSTWaL+uPOiYNT5wTpSZvz3KN1IpxZb5OpuEkm9QEHeZUXNkojP/GadgFPv3/CqCdRyzpsetYRDXwblDxz/QdZUJ90e4+A65TTzkGgUoDdbotxpmdkWFm7H6Xse9mUlWhsWTs7qRg60nxlKqQNBicUKwsiw63+13Zhp1wT6m4qfsZb8dSTt6ocO/kKeLFpv/s9xA+wJvKkw1xBvF5QkAC1TWOH4gJRNZey9MhkAcTJ7Yb4uDdEfLYYVkjuqD73TK2bKp+a0HuNS4DI6yMsXpGxJ1fz3btg8Ct8OWy+1qF7OX0kakfmulTEIWra1C3ev7rOOzJ2qOZ8WNdXIta7COROP+Z51mM3Mj3Rwb8od/t/h+JIybQfM+zVohAfQY9drLJchacQQbRsV9/YFeEpV7bXkgyYBEdnm/meUI/loltEQNkmk7IzTNBC5xtRFcIk1IbZNlwxNa60+tL1dd8Zj86FgFRHYvO/qYewgz8uZPp3QYHssHK6ON/+C9UQk9O1qi7i/fpd269gjiJHaEdK6G2DrAiP0AlPS0A0HqFU8kBMcW+DKzGlk0fbRfv4eIGmlnsRtJiSnHkI2pe9d301iZpevmK7b6t/uKoxVTHIAmMAcPOZKma0ZEioVte1jPmQ6AAn5HT+UcF0n2nLOLmjwbtR6VdbigbtqPm67rRO21jyOlqWabQWUDI1mvOvBBdM4mgk6xfukqd4g3wUQ/scXGYWBBdoTYj2TJkkA67XUmuMRhLDZwtXlEUZ2PMic8Oxo5nnZE1aKZ8W+h4O3yaOBwal19GVCloS+pVxMLdTQHpxe3emcxeAbUZO+sTCsbwvLWkxU46XceAIkDB+wYRrMdsgC8n/xaea2TlnRM6awwJRDDYyh/m1+XCIOY2ypbNIgtK9JrH98G39iUDw+ic9ApSIMkbN471kthVjc5jgsJKzsO4RAGl8CvKsqgmmKA1VSbEvGj31h6/Bi32sIjN5MGm3zwCQNt/n+G8dFSHpWBn4b723bS14GyLz/03k4H7JmI7NG6D/PJrFoDdY2afqhnO/dg5H4xdPJTrJ+x2a2JkSFVK2mM8pSXUcUAvUBi90uoEYm5dd1eqj9dlnmt3qMGUcyP8fj+57riPAE3WdaPYYjQEqe47ZFhC5A/kUyodPOwy08n6pzmgSG0B4v1mixT5CDjl1y/YsnlTIOLh3jAv4n3SsuggT2P6PP8Q1Kq0aJeBJQXUiAlenP6GfyTnTK3Ac3R5MMRyrfKpvE/Z2uBe8Bmrr6uZPHjakS1pwjb0bL44v1RyouNatSLRxqoWhvrzpYr55xXaO9gn6cXQhH80kPL480hnSqgMU6F8qCjO/9/9WSWwSoBkZzjPgTDal9e3UQZvF3hlU2tnqDvO811anspQENHZF6SU1zVLchXdtZONmnVUxaFqIvczH2sceZ4Lre5O4YT7iSjpnpk8XUlSL4IrKyE6oJ6AoaOma+KqUQqQK2bhvSzW1DJIRN1kNvk6MxWkrwQno2eP61YH7egBSzoZiqkEJoeGXtDABlgCJVNZg2mJJ1LY8tGWQl63IwSIG9aYZ4cr9A0lmBlYBSFXVeRlq8ofPrTvZiGMxzfO6BfhFAfjHVKZh764D//y/QZtE/biqTnRC84gAxipmrYOut+ClXlRJOBon7ta8o+UTxa6ZGGknG+DB0l5KNce3vNcBS3c4+G9roOu3E6yHNTXHf1B/DH5mHDVJLO/XsrcvDJtPHcw5LjB+1SHCxnIU2zh/HT08uu+cRXZ2Un06GSBCprns2ag3ZDdYLbPi5ag1qbihMBKXJuxkYClEv95F7xvDalowrzafRPE7m1fNSWkh+qnnVY7DMOX9cfo7HFJbw4picLlvETh4DyI+4tSjkZFo454UU3O3JsfNG+Mzfrbj6T+TMTOdAulWovOlpwb7Qi9lJyOLp8EJtiSa5oVWVB5rKKzXTEDgqW+67EkUKr7+KlNi/Q0x8p/E3vNbKQ8BQycYWKwcRY16D/rzy8CUdEc7fIwYYlAslSMoj5mAb0JoZZ73+G5aEtZB/zjF/1ZKCCGDhRTV4o63/Lyqo+BO9VHjczR2VS+XNDUJDGkBobBzr77FV8bI2Qq2ep1cgywWB56PNkmd5hM6gAXtTHMakj9syLmcubSVxE9JPqewfY1cKxFqs5CUAqi8amhVTKMTWWx28aZymBCBTUFVWWoKLVZCC93vvEVKYX3H/98QI23jpbNhOKMakR+aOBWMyJCIeaUsT/TKevrPPkB1OfBgbuc++/NfpDl+4IgI5I+4baNKEkxQbjtblY3SFgse91ZaAfRCz75ua1j+ukWHDPHB1bc78osVh3oI99PMq0stP9oMvFLqs7iloyPrcRWIxLH75Tx9zmQOwVwZvd4v/95nqZ/7da3ByI7ZZXb4O6PETraNXJ96wO3SCxvr9x34yqxMV/Su2r9BjtYgfO2E8jzUwJbA/8dKnUOAGuzGs30MXAqDKeSZiaDN3D60vq4xyDzcml6apGgsO+VSkan37KnHRkBI0mH9Kcpzqrb+gusgVlsy0CZIOvGa/eobx6ukJr8W5li59xL6cl79XEZ7wWYe5LsvmwDkpxVCVDk9KHKoNSePS7mvUNAzTW2ULC64I8kfcBSl0o595jd9/9aShAwzaKiUhQpHt6Tb3j6Iz53fFrd8Xd1mxsfZRAlHBFzP0xaL/P/1voJRH6rTDkAbgdjBHQ1MxAp/NTVSWzmxy6srGA7nwdFO/OZc+focGVY5WyA4JTYss6qL0B3xZ93Qaixk6UZiQWMjy7jNRimURQL8r/PaflK29moCiXM8N9JLZPq3s8DgE450pu3VQJ6R1fNkE2fmgMXzUPSidDL29Wh5Z8BPDv4O8d2dngI7pJEBWxFzQCGqcU6kkWI7MhvsG+OXrWAzfuYjI9YGPew1B91lgMl9qLfVUJU/8ghic4Ekzt8qQ1nNMzZafSPLzEO/A44nr7s8hq+nZmSrRtUUcbv7K9iZnKeG0eexvoa1DObrwTGV3jd3inHuA8a/cZAFen6rCH3ZT9LS0tw/Gs+5crD8HBW1U1CAyx4kZUWq+rq2Xc6YaDXlIUe3kLMkGsc+qn13hkl31O1ljDbr0PIINRhQqEPARVvxfqdDy8DvgqMuRjTChICMa5Wqaxg5Hzhem1w9VGMQJhmnFXKJlnoPJbbrZ8ktt2GM0nuOQOOF1SE/opW+SkJt9jzISmwSd8jZzuwoExivckCCt9I9rN9dvDXuUdT7gDFW26UOw9F7U6Zf7vqU5MZonEAh7vWwTyMTiS1hpcrMa6s3zj1C1wPWfgTru5etuuatf/06wuc7elpS8Hf13s5hZoi8kNZCGkHyQaheHcL2vAuN/bjSu+76doU5vZgyziQIMWIkNA/6vRv5yipnCKLSzm0YIGjAXIvRpCmSU7U4EJ1fOtrMcu0HEehOHE4Znu2QfIPWUFP7KYTbjiodKE7+I4tUD85EglExkwHksQ9bPmO57YBoLnqOHdOBVTf8Ev/xum7wMrIJalTvVHC2+R9lp/W9+MPW9WWTc1RJi0y0PuyIa4zgmRcxxCG/uuDasKulC2dhKQQ2h7OvGcYDozducvhV7Vz57dX+gsb1V6wLP013v8sOO6pA7HQVNUAXCSlWjrRL7/qdeDF7g==', + iv: 'xcB9YTpj5KCDyLxoYt9eqQ==', + salt: 'A4K7eVgk+QHpGdGRegzeQ2S6bjEjuTa+zupxVdVG8RE=', + }) as EncryptedData, + }, + localStorage: { + ADDRESS_BOOK_CONFIG: typedStringify([ + { + name: 'Binance', + address: 'oasis1qqekv2ymgzmd8j2s2u7g0hhc7e77e654kvwqtjwm', + }, + { + name: 'stakefish', + address: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe', + }, + ]), + LANGUAGE_CONFIG: 'en', + NETWORK_CONFIG: typedStringify({ + totalNetList: [ + { + id: 0, + name: 'Mainnet', + url: 'https://api.oasisscan.com/mainnet', + explorer: 'https://www.oasisscan.com/', + netType: 'Mainnet', + nodeType: 'DEFAULT', + grpc: 'https://grpc.oasis.dev', + isSelect: true, + }, + { + id: 1, + name: 'Testnet', + url: 'https://api.oasisscan.com/testnet', + explorer: 'https://testnet.oasisscan.com/', + netType: 'Testnet', + nodeType: 'DEFAULT', + grpc: 'https://testnet.grpc.oasis.dev', + isSelect: true, + }, + ], + currentNetList: [ + { + id: 1, + name: 'Testnet', + url: 'https://api.oasisscan.com/testnet', + explorer: 'https://testnet.oasisscan.com/', + netType: 'Testnet', + nodeType: 'DEFAULT', + grpc: 'https://testnet.grpc.oasis.dev', + isSelect: false, + }, + { + id: 0, + name: 'Mainnet', + url: 'https://api.oasisscan.com/mainnet', + explorer: 'https://www.oasisscan.com/', + netType: 'Mainnet', + nodeType: 'DEFAULT', + grpc: 'https://grpc.oasis.dev', + isSelect: true, + }, + ], + }), + DISMISSED_NEW_EXTENSION_WARNING: undefined, + }, +} satisfies WalletExtensionV0State diff --git a/src/utils/__tests__/__snapshots__/walletExtensionV0.test.ts.snap b/src/utils/__tests__/__snapshots__/walletExtensionV0.test.ts.snap new file mode 100644 index 0000000000..10e93e68ee --- /dev/null +++ b/src/utils/__tests__/__snapshots__/walletExtensionV0.test.ts.snap @@ -0,0 +1,172 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`decryptWithPasswordV0 1`] = ` +{ + "invalidPrivateKeys": [ + { + "address": "oasis1qpu7j3q3lhy0al2d4nspy0gmy9msy2s2yvap66r7", + "name": "bad privatekey1", + "privateKeyWithTypos": "1ab9bf560d4ca4044f31b99da44c5503d0e48f508c892cd82c5c4a9cfc76d1fb3e3a7c47f18a67b70410105d9444766269bb1a1e418b1cdf3a6aba8f18923d3a", + }, + ], + "language": "en", + "mnemonic": "among scrap refuse hungry remove unhappy crack horn half cruel skull project dentist poet design paper eternal stool tomato cabin helmet funny victory happy", + "state": { + "contacts": { + "oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe": { + "address": "oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe", + "name": "stakefish", + }, + "oasis1qqekv2ymgzmd8j2s2u7g0hhc7e77e654kvwqtjwm": { + "address": "oasis1qqekv2ymgzmd8j2s2u7g0hhc7e77e654kvwqtjwm", + "name": "Binance", + }, + }, + "evmAccounts": { + "0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18": { + "ethAddress": "0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18", + "ethPrivateKey": "6593a788d944bb3e25357df140fac5b0e6273f1500a3b37d6513bf9e9807afe2", + }, + }, + "network": { + "chainContext": "", + "epoch": 0, + "minimumStakingAmount": 0, + "selectedNetwork": "local", + "ticker": "", + }, + "theme": { + "selected": "light", + }, + "wallet": { + "selectedWallet": "oasis1qq30ejf9puuc6qnrazmy9dmn7f3gessveum5wnr6", + "wallets": { + "oasis1qpfltmpdyjvv88x9n7x0uvspjdtxycz9gc9zg429": { + "address": "oasis1qpfltmpdyjvv88x9n7x0uvspjdtxycz9gc9zg429", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "ledger5", + "path": [ + 44, + 474, + 0, + 0, + 5, + ], + "pathDisplay": "m/44'/474'/0'/0'/5'", + "publicKey": "75c515b91e582698550aede5bcff710394eb2d5e89780a254220f11f0976fd06", + "type": "ledger", + }, + "oasis1qpfq4k8s5r0yalyrjcrt8nu2agy4wcwen5xmukwk": { + "address": "oasis1qpfq4k8s5r0yalyrjcrt8nu2agy4wcwen5xmukwk", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "ledger1", + "path": [ + 44, + 474, + 0, + 0, + 0, + ], + "pathDisplay": "m/44'/474'/0'/0'/0'", + "publicKey": "c7875b0f3dc2fdcb7fb6c05a1a2d6c0638eed37888d593d8a90ff18190fbab44", + "type": "ledger", + }, + "oasis1qpw6nzr77u5nfucee5wjp544hzgmpjjj2gz5p6zq": { + "address": "oasis1qpw6nzr77u5nfucee5wjp544hzgmpjjj2gz5p6zq", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "Account 1", + "path": [ + 44, + 474, + 0, + ], + "pathDisplay": "m/44'/474'/0'", + "privateKey": "86b12cfbcd816983fa2ac199c21b9b217391a7345d85c0c8fc7b715fc8fae19b7d3f6555015b70642912966317a3d084d0d9670415c45084e750ff5378535737", + "publicKey": "7d3f6555015b70642912966317a3d084d0d9670415c45084e750ff5378535737", + "type": "mnemonic", + }, + "oasis1qpwlwv5y25e8h3cwd3z0glevj20y2mv5pvfw7pme": { + "address": "oasis1qpwlwv5y25e8h3cwd3z0glevj20y2mv5pvfw7pme", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "Account 2", + "path": [ + 44, + 474, + 1, + ], + "pathDisplay": "m/44'/474'/1'", + "privateKey": "c43b207bb525f5486649debeb0c8597c23db4fca0d60d6ba93b36c12b2a884186fef3b736a3286339c47f6c3fdeb0346aadc76677b7f889f6c78f768a289c1b5", + "publicKey": "6fef3b736a3286339c47f6c3fdeb0346aadc76677b7f889f6c78f768a289c1b5", + "type": "mnemonic", + }, + "oasis1qq30ejf9puuc6qnrazmy9dmn7f3gessveum5wnr6": { + "address": "oasis1qq30ejf9puuc6qnrazmy9dmn7f3gessveum5wnr6", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "short privatekey", + "privateKey": "0521f84460c6b4b18bbb1e7535dc7841e08688bf7aaea76285083c052e9b28fad8ed928b97756739db1eb2019abfaabe970b903a8e95b6dfc3e03c25559516ea", + "publicKey": "d8ed928b97756739db1eb2019abfaabe970b903a8e95b6dfc3e03c25559516ea", + "type": "private_key", + }, + "oasis1qrf4y7aelwuusc270e8qx04ysr45w3q0zyavrpdk": { + "address": "oasis1qrf4y7aelwuusc270e8qx04ysr45w3q0zyavrpdk", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "ledger5-1", + "path": [ + 44, + 474, + 0, + 0, + 6, + ], + "pathDisplay": "m/44'/474'/0'/0'/6'", + "publicKey": "603cb015cb9ec347b1f28ffa64b910b23c207c81a41f0a9b4cdbf5310b342bd3", + "type": "ledger", + }, + "oasis1qzp9vfeafqg8ejpcjl8m947weympxja4dqarmu52": { + "address": "oasis1qzp9vfeafqg8ejpcjl8m947weympxja4dqarmu52", + "balance": { + "available": null, + "debonding": null, + "delegations": null, + "total": null, + }, + "name": "private key1", + "privateKey": "0dd622997e9a8fdd7304bc1858857ac67291635bf741391a58920737b19dc717cb79bba4b4ee741688f9838ed03a5c3c8e6ae98f44fc5395cd04c75d251a90c0", + "publicKey": "cb79bba4b4ee741688f9838ed03a5c3c8e6ae98f44fc5395cd04c75d251a90c0", + "type": "private_key", + }, + }, + }, + }, +} +`; diff --git a/src/utils/__tests__/walletExtensionV0.test.ts b/src/utils/__tests__/walletExtensionV0.test.ts new file mode 100644 index 0000000000..1431a8e516 --- /dev/null +++ b/src/utils/__tests__/walletExtensionV0.test.ts @@ -0,0 +1,21 @@ +import { password, walletExtensionV0PersistedState, wrongPassword } from '../__fixtures__/test-inputs' +import { decryptWithPasswordV0 } from '../walletExtensionV0' + +let jsdomCrypto: any +beforeEach(() => { + jsdomCrypto = global.crypto + Object.defineProperty(global, 'crypto', { value: require('crypto') }) +}) + +afterEach(() => { + Object.defineProperty(global, 'crypto', { value: jsdomCrypto }) +}) + +test('decryptWithPasswordV0', async () => { + const migratedV0Fixture = await decryptWithPasswordV0(password, walletExtensionV0PersistedState) + expect(migratedV0Fixture).toMatchSnapshot() + + await expect(decryptWithPasswordV0(wrongPassword, walletExtensionV0PersistedState)).rejects.toThrow( + 'Password wrong', + ) +}) diff --git a/src/utils/walletExtensionV0.ts b/src/utils/walletExtensionV0.ts new file mode 100644 index 0000000000..6c55eb216b --- /dev/null +++ b/src/utils/walletExtensionV0.ts @@ -0,0 +1,260 @@ +import { runtimeIs } from '../config' +import { decrypt as metamaskDecrypt } from '@metamask/browser-passworder' +import { PersistedRootState } from '../app/state/persist/types' +import { initialState as initialNetworkState } from '../app/state/network' +import { Wallet, WalletType } from '../app/state/wallet/types' +import { hex2uint, uint2hex } from '../app/lib/helpers' +import { OasisKey } from '../app/lib/key' +import { PasswordWrongError } from '../types/errors' +import { LanguageKey } from '../locales/i18n' + +type EncryptedString = string & { encryptedType: T } +export type StringifiedType = string & { stringifiedType: T } + +export type EncryptedData = EncryptedString< + // Array with exactly one element + [ + { + mnemonic: EncryptedString // string contains 12 or 24 words + currentAddress: string + accounts: Array< + | { + type: 'WALLET_INSIDE' // Mnemonic + address: `oasis1${string}` + publicKey: string // 64 hex without 0x. + privateKey: EncryptedString // 128 hex without 0x. + hdPath: number + accountName: string + typeIndex: number + localAccount?: { keyringData: 'keyringData' } + isUnlocked?: true + } + | { + type: 'WALLET_OUTSIDE' // Private key + address: `oasis1${string}` + publicKey: string // 64 hex without 0x. + privateKey: EncryptedString // 128 or 64 hex without 0x. Can contain typos. + accountName: string + typeIndex: number + localAccount?: { keyringData: 'keyringData' } + isUnlocked?: true + } + | { + type: 'WALLET_LEDGER' // Ledger + address: `oasis1${string}` + publicKey: string // 64 hex without 0x. + path: [44, 474, 0, 0, number] + ledgerHdIndex: number + accountName: string + typeIndex: number + localAccount?: { keyringData: 'keyringData' } + isUnlocked?: true + } + | { + type: 'WALLET_OBSERVE' // Watch + address: `oasis1${string}` + accountName: string + typeIndex: number + localAccount?: { keyringData: 'keyringData' } + isUnlocked?: true + } + | { + type: 'WALLET_OUTSIDE_SECP256K1' // Metamask private key + address: `oasis1${string}` + publicKey: `0x${string}` // 128 hex with 0x. + evmAddress: `0x${string}` // Checksum capitalized + privateKey: EncryptedString // 64 hex without 0x. + accountName: string + typeIndex: number + localAccount?: { keyringData: 'keyringData' } + isUnlocked?: true + } + > + }, + ] +> + +export interface WalletExtensionV0State { + chromeStorageLocal: { + keyringData: undefined | EncryptedData + } + localStorage: { + ADDRESS_BOOK_CONFIG: StringifiedType> + LANGUAGE_CONFIG: undefined | 'en' | 'zh_CN' // Ignored in migration + NETWORK_CONFIG: undefined | any // Ignored in migration + DISMISSED_NEW_EXTENSION_WARNING: undefined | any // Ignored in migration + } +} + +export async function readStorageV0() { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + return { + chromeStorageLocal: { + keyringData: (await browser.storage.local.get('keyringData')).keyringData, + }, + localStorage: { + ADDRESS_BOOK_CONFIG: window.localStorage.getItem('ADDRESS_BOOK_CONFIG') ?? '[]', + LANGUAGE_CONFIG: window.localStorage.getItem('LANGUAGE_CONFIG') ?? undefined, + NETWORK_CONFIG: window.localStorage.getItem('NETWORK_CONFIG') ?? undefined, + DISMISSED_NEW_EXTENSION_WARNING: + window.localStorage.getItem('DISMISSED_NEW_EXTENSION_WARNING') ?? undefined, + }, + } as WalletExtensionV0State +} + +async function typedMetamaskDecrypt(password: string, encrypted: EncryptedString): Promise { + try { + return await metamaskDecrypt(password, encrypted) + } catch (error) { + throw new PasswordWrongError() + } +} + +function typedJsonParse(str: StringifiedType): T { + return JSON.parse(str) +} + +function validateAndExpandPrivateKey(privateKeyLongOrShortOrTyposHex: string): string | '' { + try { + return uint2hex(OasisKey.fromPrivateKey(hex2uint(privateKeyLongOrShortOrTyposHex))) + } catch (e) { + return '' + } +} + +export async function decryptWithPasswordV0(password: string, extensionV0State: WalletExtensionV0State) { + if (!extensionV0State.chromeStorageLocal.keyringData) throw new Error('No v0 encrypted data') + const keyringData = ( + await typedMetamaskDecrypt(password, extensionV0State.chromeStorageLocal.keyringData) + )[0] + const mnemonic = await typedMetamaskDecrypt(password, keyringData.mnemonic) + + const decryptedAccounts = await Promise.all( + keyringData.accounts.map(async acc => { + if (!('privateKey' in acc)) return acc + const privateKeyLongOrShortOrTypos = await typedMetamaskDecrypt(password, acc.privateKey) + if (acc.type === 'WALLET_OUTSIDE_SECP256K1') { + return { + ...acc, + privateKey: privateKeyLongOrShortOrTypos, + privateKeyLongOrShortOrTypos, + } + } + + const privateKey = validateAndExpandPrivateKey(privateKeyLongOrShortOrTypos) + return { + ...acc, + privateKey, + privateKeyLongOrShortOrTypos, + } + }), + ) + + const [validAccounts, invalidAccounts] = [ + decryptedAccounts + .map(acc => ('privateKey' in acc && acc.privateKey === '' ? undefined : acc)) + .filter((a): a is NonNullable => !!a), + decryptedAccounts + .map(acc => ('privateKey' in acc && acc.privateKey === '' ? acc : undefined)) + .filter((a): a is NonNullable => !!a), + ] + + const wallets = Object.fromEntries( + validAccounts + .map((acc): undefined | Wallet => { + if (acc.type === 'WALLET_INSIDE') { + return { + publicKey: acc.publicKey, + address: acc.address, + type: WalletType.Mnemonic, + path: [44, 474, acc.hdPath], + pathDisplay: `m/44'/474'/${acc.hdPath}'`, + privateKey: acc.privateKey, + balance: { + available: null, + debonding: null, + delegations: null, + total: null, + }, + name: acc.accountName, + } + } + if (acc.type === 'WALLET_OUTSIDE') { + return { + publicKey: acc.publicKey, + address: acc.address, + type: WalletType.PrivateKey, + privateKey: acc.privateKey, + balance: { + available: null, + debonding: null, + delegations: null, + total: null, + }, + name: acc.accountName, + } + } + if (acc.type === 'WALLET_LEDGER') { + return { + publicKey: acc.publicKey, + address: acc.address, + type: WalletType.Ledger, + path: [44, 474, 0, 0, acc.ledgerHdIndex], + pathDisplay: `m/44'/474'/0'/0'/${acc.ledgerHdIndex}'`, + balance: { + available: null, + debonding: null, + delegations: null, + total: null, + }, + name: acc.accountName, + } + } + return undefined + }) + .filter((a): a is NonNullable => !!a) + .map(a => [a.address, a]), + ) + + const invalidPrivateKeys = invalidAccounts.map(a => ({ + privateKeyWithTypos: a.privateKeyLongOrShortOrTypos, + name: a.accountName, + address: a.address, + })) + + const evmAccounts = Object.fromEntries( + validAccounts + .map(acc => { + if (acc.type !== 'WALLET_OUTSIDE_SECP256K1') return undefined + return { + ethPrivateKey: acc.privateKey, + ethAddress: acc.evmAddress, + } + }) + .filter((a): a is NonNullable => !!a) + .map(a => [a.ethAddress, a]), + ) + + const observedAccounts = validAccounts + .filter(acc => acc.type === 'WALLET_OBSERVE') + .map(acc => ({ name: acc.accountName, address: acc.address })) + const addressBookAccounts = typedJsonParse(extensionV0State.localStorage.ADDRESS_BOOK_CONFIG) + const contacts = Object.fromEntries([...observedAccounts, ...addressBookAccounts].map(a => [a.address, a])) + + const language: LanguageKey = extensionV0State.localStorage.LANGUAGE_CONFIG === 'zh_CN' ? 'zh_CN' : 'en' + + const state: PersistedRootState = { + contacts: contacts, + evmAccounts: evmAccounts, + network: initialNetworkState, + theme: { + selected: 'light', + }, + wallet: { + selectedWallet: keyringData.currentAddress, + wallets: wallets, + }, + } + return { mnemonic, invalidPrivateKeys, language, state } +} diff --git a/yarn.lock b/yarn.lock index 750a70cebc..c450a7d59d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2454,6 +2454,11 @@ resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.7.11.tgz#af2cb4ae6d3a92ecdeb1503b73079417525476d2" integrity sha512-BJwkHlSUgtB+Ei52Ai32M1AOMerSlzyIGA/KC4dAGL+GGwVMdwG8HGCOA2TxP3KjhbgDPMYkv7bt/NmOmRIFng== +"@metamask/browser-passworder@=3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@metamask/browser-passworder/-/browser-passworder-3.0.0.tgz#c06744e66a968ffa13f70cc71a7d3b15d86b0ee7" + integrity sha512-hD10mgvhcDkZX8wnauw8udp1K2MzcbZfrN7Yon9sQ+OqVK9kiQ4VhZAyZNZTy9KJLtfoVD9Y2F6L4gEese7hDA== + "@metamask/jazzicon@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/jazzicon/-/jazzicon-2.0.0.tgz#5615528e91c0fc5c9d79202d1f0954a7922525a0"