{
})
act(() => {
- setAccountForm.submit()
+ fireEvent.submit(setAccountForm)
})
act(() => {
- setAccountForm.submit()
+ fireEvent.submit(setAccountForm)
})
})
diff --git a/src/components/containers/Login/Login.css b/src/components/containers/Login/Login.css
index 8e21b376..eaa02ef6 100644
--- a/src/components/containers/Login/Login.css
+++ b/src/components/containers/Login/Login.css
@@ -4,6 +4,16 @@
align-content: center;
display: flex;
flex-direction: column;
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
}
.subtitle {
margin-top: 2rem;
diff --git a/src/components/containers/SendTransaction/AmountField.js b/src/components/containers/SendTransaction/AmountField.js
index b392b415..53336e90 100644
--- a/src/components/containers/SendTransaction/AmountField.js
+++ b/src/components/containers/SendTransaction/AmountField.js
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
import { CryptoFiatField } from '@ComposedComponents'
import TransactionField from './TransactionField'
@@ -15,11 +15,7 @@ const AmountField = ({
totalFeeInCrypto,
transactionMode,
}) => {
- const [message, setMessage] = useState(errorMessage)
-
- useEffect(() => {
- setMessage(errorMessage)
- }, [errorMessage, setMessage])
+ const [localMessage, setLocalMessage] = useState(undefined)
return (
@@ -31,14 +27,14 @@ const AmountField = ({
transactionData={transactionData}
validity={validity}
changeValueHandle={amountChanged}
- setErrorMessage={setMessage}
+ setErrorMessage={setLocalMessage}
exchangeRate={exchangeRate}
maxValueInToken={maxValueInToken}
setAmountValidity={setAmountValidity}
totalFeeInCrypto={totalFeeInCrypto}
transactionMode={transactionMode}
/>
- {message}
+ {localMessage ?? errorMessage}
)
}
diff --git a/src/components/containers/SendTransaction/FeesField.js b/src/components/containers/SendTransaction/FeesField.js
index 72690da2..5356e199 100644
--- a/src/components/containers/SendTransaction/FeesField.js
+++ b/src/components/containers/SendTransaction/FeesField.js
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
import { FeeField, FeeFieldML } from '@ComposedComponents'
import TransactionField from './TransactionField'
@@ -12,11 +12,7 @@ const FeesField = ({
setFeeValidity,
walletType,
}) => {
- const [message, setMessage] = useState(errorMessage)
-
- useEffect(() => {
- setMessage(errorMessage)
- }, [errorMessage, setMessage])
+ const [localMessage, setLocalMessage] = useState(undefined)
return (
@@ -27,7 +23,7 @@ const FeesField = ({
id="fee"
changeValueHandle={feeChanged}
value={value}
- setErrorMessage={setMessage}
+ setErrorMessage={setLocalMessage}
setFeeValidity={setFeeValidity}
/>
) : (
@@ -35,12 +31,12 @@ const FeesField = ({
id="fee"
changeValueHandle={feeChanged}
value={value}
- setErrorMessage={setMessage}
+ setErrorMessage={setLocalMessage}
setFeeValidity={setFeeValidity}
/>
)}
- {message}
+ {localMessage ?? errorMessage}
)
}
diff --git a/src/components/layouts/VerticalGroup/VerticalGroup.js b/src/components/layouts/VerticalGroup/VerticalGroup.js
index c0ae41c4..861c8828 100644
--- a/src/components/layouts/VerticalGroup/VerticalGroup.js
+++ b/src/components/layouts/VerticalGroup/VerticalGroup.js
@@ -1,5 +1,4 @@
-import React, { useEffect } from 'react'
-import { useStyleClasses } from '@Hooks'
+import React, { useMemo } from 'react'
import './VerticalGroup.css'
@@ -12,39 +11,16 @@ const VerticalGroup = ({
grow = false,
center = false,
}) => {
- const classesList = ['v-group']
- bigGap && classesList.push('bigGap')
- midGap && classesList.push('midGap')
- smallGap && classesList.push('smallGap')
- fullWidth && classesList.push('fullWidth')
- grow && classesList.push('grow')
- center && classesList.push('center')
- const { styleClasses, addStyleClass, removeStyleClass } =
- useStyleClasses(classesList)
-
- useEffect(() => {
- bigGap ? addStyleClass('bigGap') : removeStyleClass('bigGap')
- }, [bigGap, addStyleClass, removeStyleClass])
-
- useEffect(() => {
- midGap ? addStyleClass('midGap') : removeStyleClass('midGap')
- }, [midGap, addStyleClass, removeStyleClass])
-
- useEffect(() => {
- smallGap ? addStyleClass('smallGap') : removeStyleClass('smallGap')
- }, [smallGap, addStyleClass, removeStyleClass])
-
- useEffect(() => {
- fullWidth && addStyleClass('fullWidth')
- }, [fullWidth, addStyleClass, removeStyleClass])
-
- useEffect(() => {
- grow && addStyleClass('grow')
- }, [grow, addStyleClass, removeStyleClass])
-
- useEffect(() => {
- center && addStyleClass('center')
- }, [center, addStyleClass, removeStyleClass])
+ const styleClasses = useMemo(() => {
+ const classes = ['v-group']
+ if (bigGap) classes.push('bigGap')
+ if (midGap) classes.push('midGap')
+ if (smallGap) classes.push('smallGap')
+ if (fullWidth) classes.push('fullWidth')
+ if (grow) classes.push('grow')
+ if (center) classes.push('center')
+ return classes.join(' ')
+ }, [bigGap, midGap, smallGap, fullWidth, grow, center])
return (
{
const [accountID, setAccountID] = useState('')
const [accountName, setAccountName] = useState('')
const [accounts, setAccounts] = useState(null)
- const [lines, setLines] = useState([])
- const [entropy, setEntropy] = useState([])
const [balanceLoading, setBalanceLoading] = useState(false)
const [deletingAccount, setDeletingAccount] = useState(undefined)
const [removeAccountPopupOpen, setRemoveAccountPopupOpen] = useState(false)
@@ -91,10 +89,6 @@ const AccountProvider = ({ value: propValue, children }) => {
accountRegistryName,
addresses,
accountName,
- lines,
- setLines,
- entropy,
- setEntropy,
setWalletInfo: unlockAccountAndSaveParams,
isAccountUnlocked: checkAccountLockState,
setLoginTimeoutLimit,
diff --git a/src/contexts/MintlayerProvider/MintlayerProvider.js b/src/contexts/MintlayerProvider/MintlayerProvider.js
index ccd5f64c..de758d37 100644
--- a/src/contexts/MintlayerProvider/MintlayerProvider.js
+++ b/src/contexts/MintlayerProvider/MintlayerProvider.js
@@ -466,7 +466,7 @@ const MintlayerProvider = ({ value: propValue, children }) => {
const pools_data = await ML.getBatchData(uniquePools, '/pool/:address')
const emptyPoolsDataMap = uniquePools.reduce((acc, pool, index) => {
- if (pools_data[index].staker_balance.atoms === '0') {
+ if (pools_data[index]?.staker_balance?.atoms === '0') {
acc[pool] = pools_data[index]
}
return acc
diff --git a/src/hooks/UseStyleClasses/useStyleClasses.js b/src/hooks/UseStyleClasses/useStyleClasses.js
index cf87a1a1..1613dcee 100644
--- a/src/hooks/UseStyleClasses/useStyleClasses.js
+++ b/src/hooks/UseStyleClasses/useStyleClasses.js
@@ -22,7 +22,7 @@ const removeItemsFromList = (oldList = '', newList = []) => {
const useStyleClasses = (classesList = []) => {
const effectCalled = useRef(false)
- const [styleClasses, _setStyleClasses] = useState([])
+ const [styleClasses, _setStyleClasses] = useState(formatClasses(classesList))
const setStyleClasses = useCallback((classes = []) => {
_setStyleClasses(formatClasses(ensureClassesAreArray(classes)))
diff --git a/src/pages/CreateAccount/CreateAccount.js b/src/pages/CreateAccount/CreateAccount.js
index e565186f..b94b3d6d 100644
--- a/src/pages/CreateAccount/CreateAccount.js
+++ b/src/pages/CreateAccount/CreateAccount.js
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from 'react'
+import React, { useContext, useState } from 'react'
import { useNavigate } from 'react-router'
import { Loading } from '@ComposedComponents'
@@ -8,7 +8,6 @@ import { CreateAccount } from '@ContainerComponents'
import { Account, loadAccountSubRoutines } from '@Entities'
import { AccountContext } from '@Contexts'
import { BTC, BTC_ADDRESS_TYPE_ENUM } from '@Cryptos'
-import { ArrayHelper } from '@Helpers'
import './CreateAccount.css'
@@ -16,32 +15,15 @@ const CreateAccountPage = () => {
const navigate = useNavigate()
const [step, setStep] = useState(1)
const [words, setWords] = useState([])
- const { setWalletInfo, entropy, setLines, setEntropy } =
- useContext(AccountContext)
+ const { setWalletInfo } = useContext(AccountContext)
const [creatingWallet, setCreatingWallet] = useState(false)
- const generateMnemonic = async (entropy) => {
+ const generateMnemonic = async () => {
const { generateNewAccountMnemonic } = await loadAccountSubRoutines()
- const mnemonic = await generateNewAccountMnemonic(entropy)
+ const mnemonic = await generateNewAccountMnemonic()
setWords(mnemonic.split(' '))
}
- useEffect(() => {
- if (!entropy.length) return
- if (step < 3) {
- setLines([])
- setEntropy([])
- setWords([])
- }
- if (step === 4) {
- const shuffledEntropy = ArrayHelper.getNRandomElementsFromArray(
- entropy,
- 16,
- )
- generateMnemonic(shuffledEntropy)
- }
- }, [entropy, step, setLines, setEntropy])
-
const createAccount = (accountName, accountPassword, selectedWallets) => {
setCreatingWallet(true)
let accountID = null
@@ -63,8 +45,6 @@ const CreateAccountPage = () => {
setWalletInfo(addresses, accountID, name)
navigate('/dashboard')
})
- setLines([])
- setEntropy([])
}
const loadingExtraClasses = ['loading-big']
@@ -87,6 +67,7 @@ const CreateAccountPage = () => {
setStep={setStep}
words={words}
onStepsFinished={createAccount}
+ onGenerateMnemonic={generateMnemonic}
validateMnemonicFn={BTC.validateMnemonic}
defaultBTCWordList={BTC.getWordList()}
/>
diff --git a/src/pages/Home/Home.js b/src/pages/Home/Home.js
index 42abde81..b6bd04aa 100644
--- a/src/pages/Home/Home.js
+++ b/src/pages/Home/Home.js
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useRef, useState } from 'react'
+import { useContext, useEffect, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router'
import { AccountContext } from '@Contexts'
@@ -9,23 +9,23 @@ import './Home.css'
const HomePage = () => {
const effectCalled = useRef(false)
- const navigatedRef = useRef(false)
- const [unlocked, setUnlocked] = useState(false)
+ const unlockChecked = useRef(false)
const location = useLocation()
const navigate = useNavigate()
const { isAccountUnlocked, accounts, verifyAccountsExistence } =
useContext(AccountContext)
+ const unlocked = isAccountUnlocked(false)
+
useEffect(() => {
- if (navigatedRef.current) return
- const currentUnlocked = isAccountUnlocked(true)
- setUnlocked(currentUnlocked)
- if (currentUnlocked) {
- navigatedRef.current = true
+ if (unlockChecked.current) return
+ if (unlocked) {
+ unlockChecked.current = true
+ isAccountUnlocked(true)
navigate('/dashboard')
}
- }, [isAccountUnlocked, navigate])
+ }, [unlocked, isAccountUnlocked, navigate])
useEffect(() => {
if (effectCalled.current) return
@@ -33,22 +33,14 @@ const HomePage = () => {
verifyAccountsExistence()
}, [accounts, verifyAccountsExistence])
- const Home = () => {
- if (accounts === null) return
-
- return !accounts.length || location.state?.fromLogin ? (
-
- ) : (
-
- )
- }
-
- return (
- !unlocked && (
- <>
-
- >
- )
+ if (unlocked) return null
+
+ return accounts === null ? (
+
+ ) : !accounts.length || location.state?.fromLogin ? (
+
+ ) : (
+
)
}
diff --git a/src/services/Crypto/BTC/BTC.js b/src/services/Crypto/BTC/BTC.js
index a8f29c1d..45798ec9 100644
--- a/src/services/Crypto/BTC/BTC.js
+++ b/src/services/Crypto/BTC/BTC.js
@@ -12,10 +12,7 @@ const getHDWalletFromSeed = (seed) => {
return root
}
-const generateMnemonic = (entropy) => {
- const mnemonic = Bip39.entropyToMnemonic(entropy)
- return mnemonic
-}
+const generateMnemonic = () => Bip39.generateMnemonic()
const validateMnemonic = (mnemonic) => Bip39.validateMnemonic(mnemonic)
const getWordList = () => Bip39.wordlists[Bip39.getDefaultWordlist()]
diff --git a/src/services/Crypto/BTC/BTC.test.js b/src/services/Crypto/BTC/BTC.test.js
index 4590acb6..c2ab4732 100644
--- a/src/services/Crypto/BTC/BTC.test.js
+++ b/src/services/Crypto/BTC/BTC.test.js
@@ -24,7 +24,7 @@ describe('BTC Crypto Functions', () => {
})
test('generateMnemonic should create valid 12-word mnemonic', () => {
- const mnemonic = generateMnemonic(ENTROPY_DATA)
+ const mnemonic = generateMnemonic()
const words = mnemonic.split(' ')
expect(words).toHaveLength(12)
@@ -66,7 +66,7 @@ describe('BTC Crypto Functions', () => {
})
test('generated mnemonic should be valid', () => {
- const mnemonic = generateMnemonic(ENTROPY_DATA)
+ const mnemonic = generateMnemonic()
expect(validateMnemonic(mnemonic)).toBe(true)
})
})
diff --git a/src/services/Crypto/Cipher/Cipher.js b/src/services/Crypto/Cipher/Cipher.js
index 79698b3b..8f3a6fb6 100644
--- a/src/services/Crypto/Cipher/Cipher.js
+++ b/src/services/Crypto/Cipher/Cipher.js
@@ -1,11 +1,15 @@
-import { pbkdf2Sync } from 'pbkdf2'
-import {
- random as forgeRandom,
- util as forgeUtil,
- cipher as forgeCipher,
-} from 'node-forge'
-
-const ITERATIONAMOUNT = 10_000
+const V1 = { iterations: 10000 }
+const V2 = { ...V1, iterations: 600000 }
+
+const CURRENT_ENCRYPTION_VERSION = 2
+const ENCRYPTION_VERSIONS = { 1: V1, 2: V2 }
+
+const getVersionConfig = (version) => {
+ const config = ENCRYPTION_VERSIONS[version]
+ if (!config) throw new Error(`Unknown encryption version: ${version}`)
+ return config
+}
+
const KEYSIZE = 16
const IVSIZE = 12
const SALTSIZE = 16
@@ -13,25 +17,45 @@ const SALTSIZE = 16
const hexToBytes = (hexString) =>
Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
-const generateSalt = async (
- bytesAmount,
- random = forgeRandom,
- util = forgeUtil,
-) => {
- const bytes = await random.getBytes(bytesAmount)
- return util.bytesToHex(bytes)
+const binaryStringToBytes = (str) =>
+ new Uint8Array([...str].map((c) => c.charCodeAt(0)))
+
+const bytesToBinaryString = (bytes) => String.fromCharCode(...bytes)
+
+const generateSalt = async (bytesAmount) => {
+ const bytes = new Uint8Array(bytesAmount)
+ crypto.getRandomValues(bytes)
+ return Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, '0'))
+ .join('')
}
-// * KEYS are kept in memory until we manage to migrate this part to WASM
const generatePBKDF2Key = async ({
password,
salt,
- derivationFn = pbkdf2Sync,
+ version = CURRENT_ENCRYPTION_VERSION,
}) => {
+ const { iterations } = getVersionConfig(version)
const currentSalt = salt || (await generateSalt(SALTSIZE))
- const key = [
- ...derivationFn(password, currentSalt, ITERATIONAMOUNT, KEYSIZE, 'sha512'),
- ]
+ const encoder = new TextEncoder()
+ const baseKey = await crypto.subtle.importKey(
+ 'raw',
+ encoder.encode(password),
+ 'PBKDF2',
+ false,
+ ['deriveBits'],
+ )
+ const derivedBits = await crypto.subtle.deriveBits(
+ {
+ name: 'PBKDF2',
+ salt: encoder.encode(currentSalt),
+ iterations,
+ hash: 'SHA-512',
+ },
+ baseKey,
+ KEYSIZE * 8,
+ )
+ const key = [...new Uint8Array(derivedBits)]
return {
key,
@@ -39,52 +63,66 @@ const generatePBKDF2Key = async ({
}
}
-const generateIV = async (random = forgeRandom) => await random.getBytes(IVSIZE)
+const generateIV = async () => crypto.getRandomValues(new Uint8Array(IVSIZE))
-// * KEYS are kept in memory until we manage to migrate this part to WASM
-const encryptAES = async ({
- data,
- key,
- cipherFn = forgeCipher,
- util = forgeUtil,
- ivGenerationFn = generateIV,
-}) => {
- const IV = await ivGenerationFn()
- const cipher = cipherFn.createCipher('AES-GCM', key)
+const encryptAES = async ({ data, key }) => {
+ const iv = await generateIV()
const hex = Buffer.from(data).toString('hex')
- cipher.start({ iv: IV })
- cipher.update(util.createBuffer(hex))
- cipher.finish()
+ const cryptoKey = await crypto.subtle.importKey(
+ 'raw',
+ new Uint8Array(key),
+ 'AES-GCM',
+ false,
+ ['encrypt'],
+ )
+
+ const encrypted = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv },
+ cryptoKey,
+ new TextEncoder().encode(hex),
+ )
+ const result = new Uint8Array(encrypted)
return {
- encryptedData: cipher.output.getBytes(),
- iv: IV,
- tag: cipher.mode.tag.getBytes(),
+ encryptedData: bytesToBinaryString(result.slice(0, -16)),
+ iv: bytesToBinaryString(iv),
+ tag: bytesToBinaryString(result.slice(-16)),
}
}
-// * KEYS are kept in memory until we manage to migrate this part to WASM
-const decryptAES = ({
- data,
- key,
- iv,
- tag,
- cipherFn = forgeCipher,
- util = forgeUtil,
-}) => {
- const decipher = cipherFn.createDecipher('AES-GCM', key)
+const decryptAES = async ({ data, key, iv, tag }) => {
+ const cryptoKey = await crypto.subtle.importKey(
+ 'raw',
+ new Uint8Array(key),
+ 'AES-GCM',
+ false,
+ ['decrypt'],
+ )
- decipher.start({ iv, tag })
- decipher.update(util.createBuffer(data))
- const result = decipher.finish()
+ const dataBytes = binaryStringToBytes(data)
+ const tagBytes = binaryStringToBytes(tag)
+ const combined = new Uint8Array(dataBytes.length + tagBytes.length)
+ combined.set(dataBytes)
+ combined.set(tagBytes, dataBytes.length)
- if (!result) throw new Error('Incorrect password')
- return hexToBytes(decipher.output.data)
+ try {
+ const decrypted = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: binaryStringToBytes(iv) },
+ cryptoKey,
+ combined,
+ )
+ return hexToBytes(new TextDecoder().decode(decrypted))
+ } catch {
+ throw new Error('Incorrect password')
+ }
}
export {
IVSIZE,
+ CURRENT_ENCRYPTION_VERSION,
+ ENCRYPTION_VERSIONS,
+ getVersionConfig,
generateSalt,
generatePBKDF2Key,
generateIV,
diff --git a/src/services/Crypto/Cipher/Cipher.test.js b/src/services/Crypto/Cipher/Cipher.test.js
index e4909ae7..9d5bcf04 100644
--- a/src/services/Crypto/Cipher/Cipher.test.js
+++ b/src/services/Crypto/Cipher/Cipher.test.js
@@ -6,6 +6,9 @@ import {
decryptAES,
hexToBytes,
IVSIZE,
+ CURRENT_ENCRYPTION_VERSION,
+ ENCRYPTION_VERSIONS,
+ getVersionConfig,
} from './Cipher'
test('Cipher - HEX to Bytes', () => {
@@ -16,35 +19,39 @@ test('Cipher - HEX to Bytes', () => {
expect(Buffer.from(bytes).toString('hex')).toBe(hex)
})
-test('Cipher - generateSalt', async () => {
+test('Cipher - generateSalt returns correct length hex string', async () => {
const salt1 = await generateSalt(1)
const salt2 = await generateSalt(2)
+ const salt16 = await generateSalt(16)
expect(hexToBytes(salt1).length).toBe(1)
expect(hexToBytes(salt2).length).toBe(2)
+ expect(hexToBytes(salt16).length).toBe(16)
+
+ expect(typeof salt1).toBe('string')
+ expect(salt1).toMatch(/^[0-9a-f]+$/)
+ expect(salt1.length).toBe(2)
+ expect(salt2.length).toBe(4)
+ expect(salt16.length).toBe(32)
})
-test('Cipher - generateSalt random', async () => {
- const random = {
- getBytes: jest.fn(() => new Uint8Array([97])),
- }
- const salt1 = await generateSalt(1, random)
+test('Cipher - generateSalt uses crypto.getRandomValues', async () => {
+ const originalGetRandomValues = crypto.getRandomValues.bind(crypto)
+ const mockGetRandomValues = jest.fn((arr) => {
+ for (let i = 0; i < arr.length; i++) arr[i] = 0xab
+ return arr
+ })
+ crypto.getRandomValues = mockGetRandomValues
- expect(hexToBytes(salt1).length).toBe(1)
- expect(random.getBytes).toHaveBeenCalled()
-})
+ const salt = await generateSalt(3)
-test('Cipher - generateSalt utils', async () => {
- const util = {
- bytesToHex: jest.fn(() => 61),
- }
- const salt = await generateSalt(1, undefined, util)
+ expect(mockGetRandomValues).toHaveBeenCalled()
+ expect(salt).toBe('ababab')
- expect(salt).toBe(61)
- expect(util.bytesToHex).toHaveBeenCalled()
+ crypto.getRandomValues = originalGetRandomValues
})
-test('Cipher - generatePBKDF2Key', async () => {
+test('Cipher - generatePBKDF2Key deterministic with same salt', async () => {
const password = 'test'
const { key: key1, salt: salt1 } = await generatePBKDF2Key({ password })
@@ -58,69 +65,166 @@ test('Cipher - generatePBKDF2Key', async () => {
expect(salt1).toStrictEqual(salt2)
})
-test('Cipher - generatePBKDF2Key derivationFn', async () => {
+test('Cipher - generatePBKDF2Key returns correct key length', async () => {
+ const { key } = await generatePBKDF2Key({
+ password: 'test',
+ salt: 'a1b2c3d4',
+ })
+
+ expect(Array.isArray(key)).toBe(true)
+ expect(key.length).toBe(16)
+ key.forEach((byte) => {
+ expect(byte).toBeGreaterThanOrEqual(0)
+ expect(byte).toBeLessThanOrEqual(255)
+ })
+})
+
+test('Cipher - generatePBKDF2Key uses provided salt', async () => {
+ const salt = 'deadbeef'
+ const { salt: returnedSalt } = await generatePBKDF2Key({
+ password: 'test',
+ salt,
+ })
+
+ expect(returnedSalt).toBe(salt)
+})
+
+test('Cipher - generatePBKDF2Key generates salt when not provided', async () => {
+ const { salt } = await generatePBKDF2Key({ password: 'test' })
+
+ expect(typeof salt).toBe('string')
+ expect(salt).toMatch(/^[0-9a-f]+$/)
+ expect(salt.length).toBe(32)
+})
+
+test('Cipher - generatePBKDF2Key different passwords produce different keys', async () => {
+ const salt = 'fixedsalt'
+ const { key: key1 } = await generatePBKDF2Key({
+ password: 'password1',
+ salt,
+ })
+ const { key: key2 } = await generatePBKDF2Key({
+ password: 'password2',
+ salt,
+ })
+
+ expect(key1).not.toStrictEqual(key2)
+})
+
+test('Cipher - generatePBKDF2Key respects iterations parameter', async () => {
+ const salt = 'fixedsalt'
const password = 'test'
- const salt = 'salt'
- const derivationFn = () => [12]
- const { key: key1, salt: salt1 } = await generatePBKDF2Key({
+ const { key: keyDefault } = await generatePBKDF2Key({ password, salt })
+ const { key: keyLegacy } = await generatePBKDF2Key({
password,
salt,
- derivationFn,
+ version: 1,
+ })
+
+ expect(keyDefault).not.toStrictEqual(keyLegacy)
+})
+
+test('Cipher - generatePBKDF2Key known value verification', async () => {
+ const { key } = await generatePBKDF2Key({
+ password: 'testpassword',
+ salt: '0123456789abcdef0123456789abcdef',
+ version: 1,
})
- expect(key1).toStrictEqual([12])
- expect(salt1).toStrictEqual(salt)
+ expect(key.length).toBe(16)
+
+ const { key: key2 } = await generatePBKDF2Key({
+ password: 'testpassword',
+ salt: '0123456789abcdef0123456789abcdef',
+ version: 1,
+ })
+ expect(key).toStrictEqual(key2)
})
-test('Cipher - generateIV', async () => {
+test('Cipher - generateIV returns Uint8Array of correct size', async () => {
const iv = await generateIV()
+
+ expect(iv).toBeInstanceOf(Uint8Array)
expect(iv.length).toBe(IVSIZE)
+ expect(iv.length).toBe(12)
})
-test('Cipher - generateIV utils', async () => {
- const bytes = [0]
- const random = {
- getBytes: jest.fn(() => bytes),
- }
+test('Cipher - generateIV uses crypto.getRandomValues', async () => {
+ const originalGetRandomValues = crypto.getRandomValues.bind(crypto)
+ const mockGetRandomValues = jest.fn((arr) => {
+ for (let i = 0; i < arr.length; i++) arr[i] = 0x42
+ return arr
+ })
+ crypto.getRandomValues = mockGetRandomValues
+
+ const iv = await generateIV()
- const iv = await generateIV(random)
- expect(iv.length).toBe(bytes.length)
- expect(random.getBytes).toHaveBeenCalled()
+ expect(mockGetRandomValues).toHaveBeenCalled()
+ expect(iv.length).toBe(IVSIZE)
+ expect(Array.from(iv)).toStrictEqual(Array(IVSIZE).fill(0x42))
+
+ crypto.getRandomValues = originalGetRandomValues
})
-test('Cipher - encryptAES', async () => {
+test('Cipher - encryptAES returns correct structure', async () => {
const data = 'data'
- const expectedIV = 'aaaaaaaaaaaa'
- const ivGenerationFn = async () => Promise.resolve(expectedIV)
+ const key = [
+ 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
+ ]
+
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+ expect(typeof encryptedData).toBe('string')
+ expect(typeof iv).toBe('string')
+ expect(typeof tag).toBe('string')
+ expect(encryptedData.length).toBeGreaterThan(0)
+ expect(iv.length).toBe(12)
+ expect(tag.length).toBe(16)
+})
+
+test('Cipher - encryptAES deterministic with mocked IV', async () => {
+ const data = 'data'
const key = [
97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
]
- const expectedEncryptedData = new Uint8Array([
- 194, 137, 41, 21, 195, 133, 195, 144, 123, 78, 18,
- ])
+ const originalGetRandomValues = crypto.getRandomValues.bind(crypto)
+ crypto.getRandomValues = jest.fn((arr) => {
+ for (let i = 0; i < arr.length; i++) arr[i] = 0x61
+ return arr
+ })
- const expectedTag = new Uint8Array([
- 194, 131, 32, 95, 194, 170, 195, 147, 68, 195, 159, 194, 147, 194, 189, 117,
- 195, 178, 91, 50, 194, 182, 195, 128, 20,
- ])
+ const result1 = await encryptAES({ data, key })
+ const result2 = await encryptAES({ data, key })
- const { encryptedData, iv, tag } = await encryptAES({
- data,
- key,
- ivGenerationFn,
+ expect(result1.encryptedData).toBe(result2.encryptedData)
+ expect(result1.iv).toBe(result2.iv)
+ expect(result1.tag).toBe(result2.tag)
+
+ crypto.getRandomValues = originalGetRandomValues
+})
+
+test('Cipher - encryptAES different data produces different output', async () => {
+ const key = [
+ 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
+ ]
+
+ const originalGetRandomValues = crypto.getRandomValues.bind(crypto)
+ crypto.getRandomValues = jest.fn((arr) => {
+ for (let i = 0; i < arr.length; i++) arr[i] = 0x61
+ return arr
})
- expect(Buffer.from(encryptedData)).toStrictEqual(
- Buffer.from(expectedEncryptedData),
- )
- expect(iv).toBe(expectedIV)
- expect(Buffer.from(tag)).toStrictEqual(Buffer.from(expectedTag))
+ const result1 = await encryptAES({ data: 'data1', key })
+ const result2 = await encryptAES({ data: 'data2', key })
+
+ expect(result1.encryptedData).not.toBe(result2.encryptedData)
+
+ crypto.getRandomValues = originalGetRandomValues
})
-test('Cipher - decryptAES', async () => {
+test('Cipher - decryptAES round-trip', async () => {
const data = 'data'
const key = [
97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
@@ -136,7 +240,40 @@ test('Cipher - decryptAES', async () => {
expect(Buffer.from(decrypted).toString()).toBe(data)
})
-test('Cipher - decryptAES error', async () => {
+test('Cipher - decryptAES round-trip with long data', async () => {
+ const data =
+ 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
+ const key = [
+ 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
+ ]
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+ const decrypted = await decryptAES({
+ data: encryptedData,
+ iv,
+ tag,
+ key,
+ })
+
+ expect(Buffer.from(decrypted).toString()).toBe(data)
+})
+
+test('Cipher - decryptAES round-trip with unicode data', async () => {
+ const data = 'test data with special chars @#$%'
+ const key = [
+ 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
+ ]
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+ const decrypted = await decryptAES({
+ data: encryptedData,
+ iv,
+ tag,
+ key,
+ })
+
+ expect(Buffer.from(decrypted).toString()).toBe(data)
+})
+
+test('Cipher - decryptAES wrong key throws', async () => {
const data = 'data'
const key = [
97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
@@ -146,12 +283,357 @@ test('Cipher - decryptAES error', async () => {
]
const { encryptedData, iv, tag } = await encryptAES({ data, key })
- expect(
- decryptAES.bind(null, {
+ await expect(
+ decryptAES({
data: encryptedData,
iv,
tag,
key: wrongkey,
}),
- ).toThrow()
+ ).rejects.toThrow('Incorrect password')
+})
+
+test('Cipher - decryptAES tampered data throws', async () => {
+ const data = 'data'
+ const key = [
+ 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
+ ]
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+
+ const tampered =
+ String.fromCharCode(encryptedData.charCodeAt(0) ^ 0xff) +
+ encryptedData.slice(1)
+
+ await expect(
+ decryptAES({
+ data: tampered,
+ iv,
+ tag,
+ key,
+ }),
+ ).rejects.toThrow('Incorrect password')
+})
+
+test('Cipher - decryptAES tampered tag throws', async () => {
+ const data = 'data'
+ const key = [
+ 97, 98, 99, 100, 101, 97, 98, 99, 100, 101, 97, 98, 100, 101, 97, 98,
+ ]
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+
+ const tamperedTag =
+ String.fromCharCode(tag.charCodeAt(0) ^ 0xff) + tag.slice(1)
+
+ await expect(
+ decryptAES({
+ data: encryptedData,
+ iv,
+ tag: tamperedTag,
+ key,
+ }),
+ ).rejects.toThrow('Incorrect password')
+})
+
+test('Cipher - constants are correct', () => {
+ expect(CURRENT_ENCRYPTION_VERSION).toBe(2)
+ expect(getVersionConfig(1).iterations).toBe(10000)
+ expect(getVersionConfig(2).iterations).toBe(600000)
+ expect(IVSIZE).toBe(12)
+})
+
+test('Cipher - getVersionConfig throws on unknown version', () => {
+ expect(() => getVersionConfig(999)).toThrow('Unknown encryption version: 999')
+})
+
+test('Cipher - full encrypt/decrypt cycle with PBKDF2', async () => {
+ const password = 'MyStr0ng!Password'
+ const data = 'secret seed phrase data'
+
+ const { key, salt } = await generatePBKDF2Key({ password })
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+
+ const { key: key2 } = await generatePBKDF2Key({ password, salt })
+ const decrypted = await decryptAES({
+ data: encryptedData,
+ iv,
+ tag,
+ key: key2,
+ })
+
+ expect(Buffer.from(decrypted).toString()).toBe(data)
+})
+
+test('Cipher - encrypt v1, decrypt v1, re-encrypt v2, decrypt v2', async () => {
+ const password = 'MyStr0ng!Password'
+ const originalData =
+ 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
+
+ // Step 1: encrypt with v1 (10000 iterations)
+ const { key: keyV1, salt: saltV1 } = await generatePBKDF2Key({
+ password,
+ version: 1,
+ })
+ const {
+ encryptedData: encV1,
+ iv: ivV1,
+ tag: tagV1,
+ } = await encryptAES({
+ data: originalData,
+ key: keyV1,
+ })
+
+ // Step 2: decrypt with v1
+ const { key: keyV1Again } = await generatePBKDF2Key({
+ password,
+ salt: saltV1,
+ version: 1,
+ })
+ const decryptedV1 = await decryptAES({
+ data: encV1,
+ iv: ivV1,
+ tag: tagV1,
+ key: keyV1Again,
+ })
+ expect(Buffer.from(decryptedV1).toString()).toBe(originalData)
+
+ // Step 3: re-encrypt with v2 (600000 iterations)
+ const { key: keyV2, salt: saltV2 } = await generatePBKDF2Key({
+ password,
+ version: 2,
+ })
+ const {
+ encryptedData: encV2,
+ iv: ivV2,
+ tag: tagV2,
+ } = await encryptAES({
+ data: decryptedV1,
+ key: keyV2,
+ })
+
+ // Step 4: decrypt with v2
+ const { key: keyV2Again } = await generatePBKDF2Key({
+ password,
+ salt: saltV2,
+ version: 2,
+ })
+ const decryptedV2 = await decryptAES({
+ data: encV2,
+ iv: ivV2,
+ tag: tagV2,
+ key: keyV2Again,
+ })
+ expect(Buffer.from(decryptedV2).toString()).toBe(originalData)
+})
+
+test('Cipher - encrypt v2, decrypt v2, re-encrypt v1, decrypt v1', async () => {
+ const password = 'An0ther$ecure'
+ const originalData = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
+
+ // Step 1: encrypt with v2 (600000 iterations)
+ const { key: keyV2, salt: saltV2 } = await generatePBKDF2Key({
+ password,
+ version: 2,
+ })
+ const {
+ encryptedData: encV2,
+ iv: ivV2,
+ tag: tagV2,
+ } = await encryptAES({
+ data: originalData,
+ key: keyV2,
+ })
+
+ // Step 2: decrypt with v2
+ const { key: keyV2Again } = await generatePBKDF2Key({
+ password,
+ salt: saltV2,
+ version: 2,
+ })
+ const decryptedV2 = await decryptAES({
+ data: encV2,
+ iv: ivV2,
+ tag: tagV2,
+ key: keyV2Again,
+ })
+ expect(Buffer.from(decryptedV2).toString()).toBe(originalData)
+
+ // Step 3: re-encrypt with v1 (10000 iterations)
+ const { key: keyV1, salt: saltV1 } = await generatePBKDF2Key({
+ password,
+ version: 1,
+ })
+ const {
+ encryptedData: encV1,
+ iv: ivV1,
+ tag: tagV1,
+ } = await encryptAES({
+ data: decryptedV2,
+ key: keyV1,
+ })
+
+ // Step 4: decrypt with v1
+ const { key: keyV1Again } = await generatePBKDF2Key({
+ password,
+ salt: saltV1,
+ version: 1,
+ })
+ const decryptedV1 = await decryptAES({
+ data: encV1,
+ iv: ivV1,
+ tag: tagV1,
+ key: keyV1Again,
+ })
+ expect(Buffer.from(decryptedV1).toString()).toBe(originalData)
+})
+
+test('Cipher - multiple re-encryption cycles preserve data', async () => {
+ const password = 'Cycl3!Test'
+ const originalData = 'secret mnemonic phrase for multi-cycle test'
+ const versions = [1, 2, 1, 2, 1]
+
+ let currentData = originalData
+
+ for (const version of versions) {
+ const { key, salt } = await generatePBKDF2Key({ password, version })
+ const { encryptedData, iv, tag } = await encryptAES({
+ data: currentData,
+ key,
+ })
+
+ const { key: decryptKey } = await generatePBKDF2Key({
+ password,
+ salt,
+ version,
+ })
+ const decrypted = await decryptAES({
+ data: encryptedData,
+ iv,
+ tag,
+ key: decryptKey,
+ })
+
+ currentData = decrypted
+ expect(Buffer.from(decrypted).toString()).toBe(originalData)
+ }
+})
+
+test('Cipher - re-encryption with different passwords per version', async () => {
+ const originalData = 'important seed data'
+
+ // Encrypt with password1 + v1
+ const password1 = 'P@ssword1'
+ const { key: keyEnc, salt: salt1 } = await generatePBKDF2Key({
+ password: password1,
+ version: 1,
+ })
+ const {
+ encryptedData: enc1,
+ iv: iv1,
+ tag: tag1,
+ } = await encryptAES({
+ data: originalData,
+ key: keyEnc,
+ })
+
+ // Decrypt with password1 + v1
+ const { key: keyDec1 } = await generatePBKDF2Key({
+ password: password1,
+ salt: salt1,
+ version: 1,
+ })
+ const decrypted1 = await decryptAES({
+ data: enc1,
+ iv: iv1,
+ tag: tag1,
+ key: keyDec1,
+ })
+ expect(Buffer.from(decrypted1).toString()).toBe(originalData)
+
+ // Re-encrypt with password2 + v2
+ const password2 = 'N3wP@ss!'
+ const { key: keyEnc2, salt: salt2 } = await generatePBKDF2Key({
+ password: password2,
+ version: 2,
+ })
+ const {
+ encryptedData: enc2,
+ iv: iv2,
+ tag: tag2,
+ } = await encryptAES({
+ data: decrypted1,
+ key: keyEnc2,
+ })
+
+ // Decrypt with password2 + v2
+ const { key: keyDec2 } = await generatePBKDF2Key({
+ password: password2,
+ salt: salt2,
+ version: 2,
+ })
+ const decrypted2 = await decryptAES({
+ data: enc2,
+ iv: iv2,
+ tag: tag2,
+ key: keyDec2,
+ })
+ expect(Buffer.from(decrypted2).toString()).toBe(originalData)
+
+ // Old password + v2 salt should NOT decrypt
+ const { key: wrongKey } = await generatePBKDF2Key({
+ password: password1,
+ salt: salt2,
+ version: 2,
+ })
+ await expect(
+ decryptAES({ data: enc2, iv: iv2, tag: tag2, key: wrongKey }),
+ ).rejects.toThrow('Incorrect password')
+})
+
+test('Cipher - v1 key cannot decrypt v2-encrypted data with same password', async () => {
+ const password = 'SameP@ss1'
+ const originalData = 'cross version test'
+ const salt = await generateSalt(16)
+
+ const { key: keyV2 } = await generatePBKDF2Key({
+ password,
+ salt,
+ version: 2,
+ })
+ const { encryptedData, iv, tag } = await encryptAES({
+ data: originalData,
+ key: keyV2,
+ })
+
+ const { key: keyV1 } = await generatePBKDF2Key({
+ password,
+ salt,
+ version: 1,
+ })
+
+ // v1 key differs from v2 key (different iterations), so decryption must fail
+ expect(keyV1).not.toStrictEqual(keyV2)
+ await expect(
+ decryptAES({ data: encryptedData, iv, tag, key: keyV1 }),
+ ).rejects.toThrow('Incorrect password')
+})
+
+test('Cipher - full cycle with wrong password fails', async () => {
+ const data = 'secret seed phrase data'
+
+ const { key } = await generatePBKDF2Key({ password: 'correct' })
+ const { encryptedData, iv, tag } = await encryptAES({ data, key })
+
+ const { key: wrongKey } = await generatePBKDF2Key({
+ password: 'wrong',
+ salt: 'differentsalt',
+ })
+
+ await expect(
+ decryptAES({
+ data: encryptedData,
+ iv,
+ tag,
+ key: wrongKey,
+ }),
+ ).rejects.toThrow('Incorrect password')
})
diff --git a/src/services/Crypto/Cipher/Cipher.worker.js b/src/services/Crypto/Cipher/Cipher.worker.js
index 44380cf2..9f7bf63f 100644
--- a/src/services/Crypto/Cipher/Cipher.worker.js
+++ b/src/services/Crypto/Cipher/Cipher.worker.js
@@ -22,10 +22,7 @@ self.onmessage = async ({ data }) => {
if (!isValidJob(data.job)) return false
try {
- let jobResult
- data.job === CipherWorkerEnum.DECRYPT_AES
- ? (jobResult = CipherWorkerJobs[data.job](data.data))
- : (jobResult = await CipherWorkerJobs[data.job](data.data))
+ const jobResult = await CipherWorkerJobs[data.job](data.data)
postMessage(jobResult)
diff --git a/src/services/Entity/Account/Account.js b/src/services/Entity/Account/Account.js
index d502a65c..d4a402ca 100644
--- a/src/services/Entity/Account/Account.js
+++ b/src/services/Entity/Account/Account.js
@@ -8,6 +8,9 @@ import {
import { BTC as BtcHelpers } from '@Helpers'
import loadAccountSubRoutines from './loadWorkers'
import { LocalStorageService } from '@Storage'
+import { CURRENT_ENCRYPTION_VERSION } from '../../Crypto/Cipher/Cipher'
+
+const getAccountVersion = (account) => account.encryptionVersion || 1
const saveAccount = async (data) => {
const { generateEncryptionKey } = await loadAccountSubRoutines()
@@ -28,6 +31,7 @@ const saveAccount = async (data) => {
const account = {
name,
salt,
+ encryptionVersion: CURRENT_ENCRYPTION_VERSION,
iv: { btcIv, mlTestnetPrivKeyIv, mlMainnetPrivKeyIv },
tag: { btcTag, mlTestnetPrivKeyTag, mlMainnetPrivKeyTag },
seed: {
@@ -88,6 +92,7 @@ const checkPasswordValidity = async (id, password) => {
const { key } = await generateEncryptionKey({
password,
salt: account.salt,
+ version: getAccountVersion(account),
})
const decrypted = await decryptSeed({
@@ -117,6 +122,7 @@ const unlockHtlsSecret = async ({ accountId, password, hash }) => {
const { key } = await generateEncryptionKey({
password,
salt: account.salt,
+ version: getAccountVersion(account),
})
const data = account.htlsSecrets[hash]
@@ -146,6 +152,7 @@ const saveProvidedHtlsSecret = async ({ accountId, password, data }) => {
password,
account.salt,
data.secret,
+ getAccountVersion(account),
)
const updatedHtlsSecrets = {
@@ -156,6 +163,82 @@ const saveProvidedHtlsSecret = async ({ accountId, password, data }) => {
await updateAccount(accountId, { htlsSecrets: updatedHtlsSecrets })
}
+const reEncryptAccount = async (id, password, account, decryptedSeeds) => {
+ const { generateEncryptionKey, encryptSeed } = await loadAccountSubRoutines()
+
+ const { key: newKey, salt: newSalt } = await generateEncryptionKey({
+ password,
+ version: CURRENT_ENCRYPTION_VERSION,
+ })
+
+ const reEncrypt = async (data) => {
+ const { encryptedData, iv, tag } = await encryptSeed({ data, key: newKey })
+ return { encryptedData, iv, tag }
+ }
+
+ const {
+ encryptedData: btcEncryptedSeed,
+ iv: btcIv,
+ tag: btcTag,
+ } = await reEncrypt(decryptedSeeds.seed)
+
+ const {
+ encryptedData: encryptedMlTestnetPrivateKey,
+ iv: mlTestnetPrivKeyIv,
+ tag: mlTestnetPrivKeyTag,
+ } = await reEncrypt(decryptedSeeds.mlTestnetPrivateKey)
+
+ const {
+ encryptedData: encryptedMlMainnetPrivateKey,
+ iv: mlMainnetPrivKeyIv,
+ tag: mlMainnetPrivKeyTag,
+ } = await reEncrypt(decryptedSeeds.mlMainnetPrivateKey)
+
+ // Re-encrypt HTLS secrets if any
+ const updatedHtlsSecrets = {}
+ if (account.htlsSecrets) {
+ const { decryptSeed } = await loadAccountSubRoutines()
+ const { key: oldKey } = await generateEncryptionKey({
+ password,
+ salt: account.salt,
+ version: getAccountVersion(account),
+ })
+
+ for (const [hash, data] of Object.entries(account.htlsSecrets)) {
+ const decryptedSecret = await decryptSeed({
+ data: data.encryptedHtlsSecret,
+ iv: data.htlsIv,
+ tag: data.htlsTag,
+ key: oldKey,
+ })
+ const {
+ encryptedData: encryptedHtlsSecret,
+ iv: htlsIv,
+ tag: htlsTag,
+ } = await reEncrypt(decryptedSecret)
+ updatedHtlsSecrets[hash] = {
+ encryptedHtlsSecret,
+ htlsIv,
+ htlsTag,
+ txHash: data.txHash,
+ }
+ }
+ }
+
+ await updateAccount(id, {
+ salt: newSalt,
+ encryptionVersion: CURRENT_ENCRYPTION_VERSION,
+ iv: { btcIv, mlTestnetPrivKeyIv, mlMainnetPrivKeyIv },
+ tag: { btcTag, mlTestnetPrivKeyTag, mlMainnetPrivKeyTag },
+ seed: {
+ btcEncryptedSeed,
+ encryptedMlTestnetPrivateKey,
+ encryptedMlMainnetPrivateKey,
+ },
+ htlsSecrets: updatedHtlsSecrets,
+ })
+}
+
const unlockAccount = async (id, password, { wallets } = {}) => {
const storedNetworkType = LocalStorageService.getItem('networkType')
@@ -170,9 +253,12 @@ const unlockAccount = async (id, password, { wallets } = {}) => {
if (!account.walletsToCreate)
updateAccount(id, { walletsToCreate: AppInfo.DEFAULT_WALLETS_TO_CREATE })
+ const accountVersion = getAccountVersion(account)
+
const { key } = await generateEncryptionKey({
password,
salt: account.salt,
+ version: getAccountVersion(account),
})
const seed = await decryptSeed({
@@ -239,6 +325,15 @@ const unlockAccount = async (id, password, { wallets } = {}) => {
}
}
+ // Migrate old accounts to current encryption version in the background
+ if (accountVersion !== CURRENT_ENCRYPTION_VERSION) {
+ reEncryptAccount(id, password, account, {
+ seed,
+ mlTestnetPrivateKey,
+ mlMainnetPrivateKey,
+ }).catch((e) => console.error('Encryption migration failed:', e))
+ }
+
return {
addresses,
btcPrivateKeys: { btcHDWallet, btcAddressData },
diff --git a/src/services/Entity/Account/Account.worker.js b/src/services/Entity/Account/Account.worker.js
index a133a12f..96f527fc 100644
--- a/src/services/Entity/Account/Account.worker.js
+++ b/src/services/Entity/Account/Account.worker.js
@@ -6,12 +6,11 @@ const getWalletWorker = () =>
const getCipherWorker = () =>
new Worker(new URL('../../Crypto/Cipher/Cipher.worker', import.meta.url))
-const generateNewAccountMnemonic = (entropy) => {
+const generateNewAccountMnemonic = () => {
return new Promise((resolve) => {
const worker = getWalletWorker()
worker.postMessage({
job: WalletWorkerEnum.GENERATE_MNEMONIC,
- data: entropy,
})
worker.onmessage = ({ data }) => {
worker.terminate()
diff --git a/src/services/Entity/Account/AccountHelpers.js b/src/services/Entity/Account/AccountHelpers.js
index dbdb724f..580f81de 100644
--- a/src/services/Entity/Account/AccountHelpers.js
+++ b/src/services/Entity/Account/AccountHelpers.js
@@ -53,9 +53,9 @@ const getEncryptedPrivateKeys = async (password, salt, mnemonic) => {
}
}
-const getEncryptedHtlsSecret = async (password, salt, secret) => {
+const getEncryptedHtlsSecret = async (password, salt, secret, version) => {
const { generateEncryptionKey, encryptSeed } = await loadAccountSubRoutines()
- const { key } = await generateEncryptionKey({ password, salt })
+ const { key } = await generateEncryptionKey({ password, salt, version })
const {
encryptedData: encryptedHtlsSecret,
diff --git a/src/setupTests.js b/src/setupTests.js
index f61ca1e6..72aeee58 100644
--- a/src/setupTests.js
+++ b/src/setupTests.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-undef */
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
@@ -15,9 +16,11 @@ global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
// Crypto polyfill for Node.js environment
+const { webcrypto } = require('crypto')
if (typeof global.crypto === 'undefined') {
- const { webcrypto } = require('crypto')
global.crypto = webcrypto
+} else if (typeof global.crypto.subtle === 'undefined') {
+ global.crypto.subtle = webcrypto.subtle
}
// Buffer polyfill
diff --git a/src/utils/Constants/AppInfo/AppInfo.js b/src/utils/Constants/AppInfo/AppInfo.js
index cfa67111..2d4678d8 100644
--- a/src/utils/Constants/AppInfo/AppInfo.js
+++ b/src/utils/Constants/AppInfo/AppInfo.js
@@ -11,7 +11,6 @@ const appAccounts = async () => {
const decimalSeparator = '.'
const thousandsSeparator = ' '
const amountRegex = /^\d+(.\d+)?$/
-const minEntropyLength = 192
const DEFAULT_WALLETS_TO_CREATE = ['btc', 'ml']
const ML_ATOMS_PER_COIN = 100000000000
const DEFAULT_ML_WALLET_OFFSET = 21
@@ -26,9 +25,9 @@ const BATCH_REQUEST_BITCOIN_LIMIT = 10
const ML_EXPLORER_MAINNET = 'https://explorer.mintlayer.org/'
const ML_EXPLORER_TESTNET = 'https://lovelace.explorer.mintlayer.org/'
const BTC_EXPLORER_MAINNET = 'https://blockstream.info/'
-const BTC_EXPLORER_TESTNET = 'https://blockstream.info/testnet/'
+const BTC_EXPLORER_TESTNET = 'https://explorer.gomaestro.org/bitcoin/testnet/'
const BTC_DEFAULT_ADDRESSES_BATCH = 3
-const BTC_MAX_TRANSACTION_FEE = 100_000 // 0.001 BTC
+const BTC_MAX_TRANSACTION_FEE = 100000 // 0.001 BTC
const BTC_MAX_FEERATE = 200
const COLOR_LIST = {
btc: '#F7931A',
@@ -125,6 +124,12 @@ const WALLETS_NAVIGATION = [
},
]
+const WALLET_NAME_ERROR = 'The wallet name should have at least 4 characters.'
+const WALLET_PASSWORD_ERROR = [
+ 'Your password should have at least 8 characters.',
+ 'Also it should have a lowercase letter, an uppercase letter, a digit, and a special char like: /\\*()&^%$#@-_=+\'"?!:;<>~`',
+]
+
const MAX_ML_FEE = 500000000000 // 5 ML in atoms
const REFRESH_INTERVAL = 1000 * 60 * 2 // one per two minutes
@@ -133,7 +138,6 @@ export {
decimalSeparator,
thousandsSeparator,
amountRegex,
- minEntropyLength,
walletTypes,
DEFAULT_WALLETS_TO_CREATE,
NETWORK_TYPES,
@@ -160,4 +164,6 @@ export {
BTC_MAX_TRANSACTION_FEE,
BTC_MAX_FEERATE,
COLOR_LIST,
+ WALLET_NAME_ERROR,
+ WALLET_PASSWORD_ERROR,
}
diff --git a/tests/01-create-account.spec.js b/tests/01-create-account.spec.js
index a2338070..94ad1957 100644
--- a/tests/01-create-account.spec.js
+++ b/tests/01-create-account.spec.js
@@ -30,57 +30,6 @@ test('Create account', async ({ page }) => {
await page.fill('input[placeholder="Password"]', WALLET_PASSWORD)
await page.getByRole('button', { name: 'Continue' }).click()
- await expect(
- page.locator(
- ':text("In the blank screen aside please draw anything you want.")',
- ),
- ).toBeVisible()
-
- await expect(
- page.locator(
- ':text("We are going to use this drawing to generate a random seed for your wallet.")',
- ),
- ).toBeVisible()
-
- await expect(
- page.locator(
- ':text("The more random the drawing is, the more secure your wallet will be.")',
- ),
- ).toBeVisible()
-
- await expect(page.locator(':text("Express your art.")')).toBeVisible()
-
- // Helper functions
- const getRandomIntBetween = (min, max) => {
- return Math.floor(Math.random() * (max - min + 1)) + min
- }
-
- const drawRandomEntropy = async (page, element) => {
- const el = await page.$(element)
- const coords = await el.boundingBox()
- const gap = 50
- const limits = {
- minX: coords.x + gap,
- maxX: coords.x + coords.width - gap,
- minY: coords.y + gap,
- maxY: coords.y + coords.height - gap,
- }
- for (let i = 0; i < 55; i++) {
- const x = getRandomIntBetween(limits.minX, limits.maxX)
- const y = getRandomIntBetween(limits.minY, limits.maxY)
- await page.mouse.move(x, y)
- await page.mouse.down()
- await page.mouse.move(x + 10, y + 10) // move a little bit while the mouse button is down
- await page.mouse.up()
- }
- }
-
- const drawingBoardStage = 'div.drawingBoard'
-
- await drawRandomEntropy(page, drawingBoardStage)
-
- await page.getByRole('button', { name: 'Continue' }).click()
-
await expect(
page.locator(
':text("Write down each of the words (seed phrases) that are shown on the next screen.")',
diff --git a/webpack.config.js b/webpack.config.js
index 489432c9..3cd61a57 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -66,6 +66,9 @@ module.exports = {
},
hot: true,
port: process.env.PORT || 3000,
+ client: {
+ webSocketURL: 'auto://0.0.0.0:0/ws',
+ },
open: true,
historyApiFallback: {
disableDotRule: true,