Skip to content

Commit

Permalink
Add utils for migration from V0 encrypted state to RootState
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaw3d committed Nov 24, 2023
1 parent 422edbb commit a4a1fc5
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 0 deletions.
1 change: 1 addition & 0 deletions .changelog/1771.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add utils for migration from V0 encrypted state to RootState
172 changes: 172 additions & 0 deletions src/utils/__tests__/__snapshots__/walletExtensionV0.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
},
}
`;
21 changes: 21 additions & 0 deletions src/utils/__tests__/walletExtensionV0.test.ts
Original file line number Diff line number Diff line change
@@ -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',
)
})
164 changes: 164 additions & 0 deletions src/utils/walletExtensionV0.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
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<T> = string & { encryptedType: T }
export type StringifiedType<T> = string & { stringifiedType: T }
Expand Down Expand Up @@ -94,3 +102,159 @@ export async function readStorageV0() {
},
} as WalletExtensionV0State
}

async function typedMetamaskDecrypt<T>(password: string, encrypted: EncryptedString<T>): Promise<T> {
try {
return await metamaskDecrypt(password, encrypted)
} catch (error) {
throw new PasswordWrongError()
}
}

function typedJsonParse<T>(str: StringifiedType<T>): 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<typeof a> => !!a),
decryptedAccounts
.map(acc => ('privateKey' in acc && acc.privateKey === '' ? acc : undefined))
.filter((a): a is NonNullable<typeof a> => !!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<typeof a> => !!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<typeof a> => !!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 }
}

0 comments on commit a4a1fc5

Please sign in to comment.