Skip to content

Commit

Permalink
test: unit test for password recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
schmanu committed Sep 19, 2023
1 parent e13c02e commit f8e5731
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 14 deletions.
6 changes: 0 additions & 6 deletions public/serviceworker/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,6 @@ self.addEventListener('fetch', function (event) {
bc.addEventListener("message", function (ev) {
if (ev.success) {
bc.close();
console.log("posted", {
queryParams,
instanceParams,
hashParams,
});
} else {
window.close();
showCloseText();
Expand Down Expand Up @@ -318,7 +313,6 @@ self.addEventListener('fetch', function (event) {
)
}
} catch (error) {
console.log('Hello')
console.error(error)
}
})
90 changes: 90 additions & 0 deletions src/hooks/wallets/mpc/recovery/__tests__/fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"fixtureInitializedLocalShare": {
"metadata": {
"pubKey": "0303baa38e308f1e6775c3d0ae66fa7dfe52f1fb8824ac9c85643f1872a8c9c467",
"polyIDList": [
"0303baa38e308f1e6775c3d0ae66fa7dfe52f1fb8824ac9c85643f1872a8c9c467|0269ba52992895dc8a91d383911b5e32420c5055930946c96ecd9d29e140457280|0x0|1|72e22f98592e2166ec6cbde1ad439b69cc33ead59e9416c38e3c7a92e0e1222a"
],
"scopedStore": {},
"generalStore": {
"shareDescriptions": {
"044c7cc4da3ebd1f24bc9dac16d50ab7bee494f310646118713c7446c92076a433b6dcf5d2ff041e95ca630bf0c48e4d1759ea0f68203a3b0f3f2452d21b9c9e31": [
"{\"module\":\"local storage share\",\"dateAdded\":1695121500805,\"tssShareIndex\":2}"
]
}
},
"tkeyStore": {},
"nonce": 1,
"tssNonces": {
"default": 0
},
"tssPolyCommits": {
"default": [
{
"x": "3da18b634da037495719ebc081ef78b25becec1e9fdc211526d545a389054c39",
"y": "43795322b08a296a0a47a9dd3c4b98dbde608fd4ba0e260ad659a31d1513ddff"
},
{
"x": "6b789875bc77890707dba902058579993ced32c1e1f6402a8c4b28ebc820f1ec",
"y": "4cdd97f4feac7e3103e3edc07209efc6a2aa98f1fee66b2e81739f9a458a76bb"
}
]
},
"factorPubs": {
"default": [
{
"x": "4c7cc4da3ebd1f24bc9dac16d50ab7bee494f310646118713c7446c92076a433",
"y": "b6dcf5d2ff041e95ca630bf0c48e4d1759ea0f68203a3b0f3f2452d21b9c9e31"
}
]
},
"factorEncs": {
"default": {
"4c7cc4da3ebd1f24bc9dac16d50ab7bee494f310646118713c7446c92076a433": {
"tssIndex": 2,
"type": "direct",
"userEnc": {
"ciphertext": "f5d18c9ad0bf6439270c30e178e3631a5bcce86e2fed02e0679c8dd5484e25b075919818d1a089be9e6d94190e278b2f",
"ephemPublicKey": "04c7302848a3fa9cfb2ce23578efb6cfe3e1618ebe90e7ef31c6af1a79c548507fbfbe537c838447d8d6f235e0dca10bed3d1d6bcd5b5083c9d15c3ddc89fdaa21",
"iv": "ec3eefa561a4d7b452d68e9eb632c173",
"mac": "b3ff0137e344f306cf04231030bcbe3301b5e82f6f6dfc189689687c6567199b"
},
"serverEncs": []
}
}
},
"publicPolynomials": {
"0303baa38e308f1e6775c3d0ae66fa7dfe52f1fb8824ac9c85643f1872a8c9c467|0269ba52992895dc8a91d383911b5e32420c5055930946c96ecd9d29e140457280": {
"polynomialCommitments": [
{
"x": "3baa38e308f1e6775c3d0ae66fa7dfe52f1fb8824ac9c85643f1872a8c9c467",
"y": "4e67bd28a72577e782f44216af96af715768aaa704dac7c57359748ee3705d3f"
},
{
"x": "69ba52992895dc8a91d383911b5e32420c5055930946c96ecd9d29e140457280",
"y": "7fee8884841cf0fa125a5baf30340d11efacfab5074bfc3d78621b478db30de"
}
]
}
}
},
"shares": {
"1": {
"share": {
"share": "fc0d2f70bbe19c383d4daca07bf0b8ef4cd6054da84370c1f8150dc115a2f8ff",
"shareIndex": "1"
},
"polynomialID": "0303baa38e308f1e6775c3d0ae66fa7dfe52f1fb8824ac9c85643f1872a8c9c467|0269ba52992895dc8a91d383911b5e32420c5055930946c96ecd9d29e140457280"
},
"2": {
"share": {
"share": "790a32257536e02db04c18a8a5a0b04661dcc32c416c362e6ba689b83d42870b",
"shareIndex": "72e22f98592e2166ec6cbde1ad439b69cc33ead59e9416c38e3c7a92e0e1222a"
},
"polynomialID": "0303baa38e308f1e6775c3d0ae66fa7dfe52f1fb8824ac9c85643f1872a8c9c467|0269ba52992895dc8a91d383911b5e32420c5055930946c96ecd9d29e140457280"
}
},
"localFactorKey": "fc8c8451a4c02775b90dab5533372702774336b3256fd8f523ccfed04ab01107",
"privateKey": "310b11bc6da78b5faec249ecf18d2dd8bcabbf4e6abec65987ed0267a3f0d5ca"
}
}
87 changes: 87 additions & 0 deletions src/hooks/wallets/mpc/recovery/__tests__/mockTKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { generatePrivate } from 'eccrypto'
import ThresholdKey, { type Metadata } from '@tkey-mpc/core'

import BN from 'bn.js'
import { getPubKeyPoint, type ShareStore, type PolynomialID } from '@tkey-mpc/common-types'

export type TKeySetupParameters = {
/** Initial metadata after initializing tKey */
metadata: Metadata
/** First share store */
share1: ShareStore
/** Second share store */
share2: ShareStore
/** TKey's private key */
privateKey: string
}

export class MockThresholdKey extends ThresholdKey {
private static INSTANCE: MockThresholdKey | undefined

private initialized = false
private setupParams: TKeySetupParameters

private constructor(setupParams: TKeySetupParameters) {
super({
manualSync: true,
enableLogging: true,
})
this.setupParams = setupParams
this.init()
MockThresholdKey.INSTANCE = this
}

isInitialized() {
return this.initialized
}

static getInstance() {
return MockThresholdKey.INSTANCE
}

static createInstance(setupParams: TKeySetupParameters) {
return new MockThresholdKey(setupParams)
}

static resetInstance() {
MockThresholdKey.INSTANCE = undefined
}

async init() {
const factorKey = new BN(generatePrivate())
const deviceTSSShare = new BN(generatePrivate())
const deviceTSSIndex = 2
const factorPub = getPubKeyPoint(factorKey)

await this.initialize({
useTSS: true,
factorPub,
deviceTSSShare,
deviceTSSIndex,
withShare: this.setupParams.share1,
})
await this._setKey(new BN(this.setupParams.privateKey, 'hex'))
await this.inputShareStoreSafe(this.setupParams.share2)

this.initialized = true
}

syncLocalMetadataTransitions(): Promise<void> {
return Promise.resolve()
}

_syncShareMetadata(adjustScopedStore?: ((ss: unknown) => unknown) | undefined): Promise<void> {
return Promise.resolve()
}

async catchupToLatestShare(params: {
shareStore: ShareStore
polyID?: PolynomialID
includeLocalMetadataTransitions?: boolean
}) {
return Promise.resolve({
latestShare: params.shareStore,
shareMetadata: this.setupParams.metadata.clone(),
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { renderHook, waitFor } from '@testing-library/react'
import { Metadata } from '@tkey-mpc/core'
import fixtures from './fixtures.json'

import BN from 'bn.js'
import { getPubKeyPoint, ShareStore } from '@tkey-mpc/common-types'
import { usePasswordRecovery } from '../usePasswordRecovery'
import { MockThresholdKey, type TKeySetupParameters } from './mockTKey'

const localShareSetupParams: TKeySetupParameters = {
metadata: Metadata.fromJSON(fixtures.fixtureInitializedLocalShare.metadata),
share1: ShareStore.fromJSON(fixtures.fixtureInitializedLocalShare.shares[1]),
share2: ShareStore.fromJSON(fixtures.fixtureInitializedLocalShare.shares[2]),
privateKey: fixtures.fixtureInitializedLocalShare.privateKey,
}

jest.mock('@/hooks/wallets/mpc/useMPC', () => ({
__esModule: true,
default: jest.fn(() => MockThresholdKey.getInstance()),
}))

describe('usePasswordRecovery', () => {
it('should setup password recovery and recover the factorKey correctly', async () => {
MockThresholdKey.resetInstance()
const tKey = MockThresholdKey.createInstance(localShareSetupParams)
await waitFor(() => {
expect(tKey.isInitialized()).toBeTruthy()
})

const TEST_PASSWORD = 'deadbeef'
const localFactorKey = new BN(fixtures.fixtureInitializedLocalShare.localFactorKey, 'hex')

const { result } = renderHook(() => usePasswordRecovery(localFactorKey))
await result.current.upsertPasswordBackup(TEST_PASSWORD)

const question = 'ENTER PASSWORD'
const questionEntryString = tKey.metadata.getShareDescription()[question]
expect(questionEntryString).toBeDefined()
expect(questionEntryString).toHaveLength(1)
const questionEntry = JSON.parse(questionEntryString[0])

expect(questionEntry.module).toBe('securityQuestions')
expect(questionEntry.associatedFactor).toBeDefined()

// Try to decrypt it with the password Hash and check that the public key is in the share store
const recoveredFactorKey = await result.current.recoverPasswordFactorKey(TEST_PASSWORD)

const restoredPubKey = getPubKeyPoint(recoveredFactorKey)
const shareDescriptions = tKey.metadata.getShareDescription()

const x = restoredPubKey.x.toString('hex')
const y = restoredPubKey.y.toString('hex')
expect(shareDescriptions[`04${x}${y}`]).toBeDefined()
})

it('should throw if recovering without a password recovery setup', async () => {
MockThresholdKey.resetInstance()
const tKey = MockThresholdKey.createInstance(localShareSetupParams)
await waitFor(() => {
expect(tKey.isInitialized()).toBeTruthy()
})

const localFactorKey = new BN(fixtures.fixtureInitializedLocalShare.localFactorKey, 'hex')

const { result } = renderHook(() => usePasswordRecovery(localFactorKey))

// Try to decrypt it with the password Hash and check that the public key is in the share store
expect(result.current.recoverPasswordFactorKey('Some password')).rejects.toEqual(
new Error('No password backup found'),
)
})

it('Should not be able to recover using an invalid password', async () => {
MockThresholdKey.resetInstance()
const tKey = MockThresholdKey.createInstance(localShareSetupParams)
await waitFor(() => {
expect(tKey.isInitialized()).toBeTruthy()
})

const TEST_PASSWORD = 'deadbeef'
const localFactorKey = new BN(fixtures.fixtureInitializedLocalShare.localFactorKey, 'hex')

const { result } = renderHook(() => usePasswordRecovery(localFactorKey))
await result.current.upsertPasswordBackup(TEST_PASSWORD)

const question = 'ENTER PASSWORD'
const questionEntryString = tKey.metadata.getShareDescription()[question]
expect(questionEntryString).toBeDefined()
expect(questionEntryString).toHaveLength(1)
const questionEntry = JSON.parse(questionEntryString[0])

expect(questionEntry.module).toBe('securityQuestions')
expect(questionEntry.associatedFactor).toBeDefined()

expect(result.current.recoverPasswordFactorKey('Wrong password')).rejects.toEqual(
new Error('Unable to decrypt using the entered password.'),
)
})
})
21 changes: 15 additions & 6 deletions src/hooks/wallets/mpc/recovery/usePasswordRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ import { keccak256 } from 'ethereum-cryptography/keccak'

const question = 'ENTER PASSWORD'

export const answerToUserInputHashBN = (answerString: string): BN => {
/**
* Creates private key for a user input password
* @param answerString password
* @returns private key as BN
*/
export const answerToUserInputHashBN = (password: string): BN => {
// TODO: Should we use a proper password hashing algorithm?
return new BN(keccak256(Buffer.from(answerString, 'utf8')))
// keccak256
return new BN(keccak256(new Uint8Array(Buffer.from(password, 'utf8'))))
}

export const usePasswordRecovery = (localFactorKey: BN | null) => {
Expand Down Expand Up @@ -96,10 +102,13 @@ export const usePasswordRecovery = (localFactorKey: BN | null) => {
}

const passwordBN = answerToUserInputHashBN(password)
const factorKeyBuffer = await decrypt(toPrivKeyECC(passwordBN), passwordShare)
const factorKey = new BN(Buffer.from(factorKeyBuffer).toString('hex'), 'hex')

return factorKey
try {
const factorKeyBuffer = await decrypt(toPrivKeyECC(passwordBN), passwordShare)
const factorKey = new BN(Buffer.from(factorKeyBuffer).toString('hex'), 'hex')
return factorKey
} catch (error) {
throw new Error('Unable to decrypt using the entered password.')
}
}

return {
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/wallets/mpc/useMPCWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const useMPCWallet = () => {
if (signingParams) {
localSetup().then(() => setWalletState(MPCWalletState.READY))
}
}, [chain, loginResponse, signingParams, user?.email, walletAddress])
}, [chain, loginResponse, signingParams, tKey, user?.email, walletAddress])

const resetAccount = async () => {
if (!loginResponse || !tKey) {
Expand Down Expand Up @@ -348,7 +348,6 @@ export const useMPCWallet = () => {
if (!onboard) {
return
}
console.log('Connecting to onboard MPC module')
connectWallet(onboard, {
autoSelect: {
label: ONBOARD_MPC_MODULE_LABEL,
Expand Down

0 comments on commit f8e5731

Please sign in to comment.