Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/controllers/emailVault/emailVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ describe('happy cases', () => {
test('upload keystore secret', async () => {
const ev = new EmailVaultController(storageCtrl, fetch, relayerUrl, keystore, testingOptions)
await ev.getEmailVaultInfo(email)
expect(Object.keys(ev.emailVaultStates.email[email].availableSecrets).length).toBe(1)
expect(Object.keys(ev.emailVaultStates.email[email]!.availableSecrets).length).toBe(1)
await ev.uploadKeyStoreSecret(email)
const newSecrets = ev.emailVaultStates.email[email].availableSecrets
const newSecrets = ev.emailVaultStates.email[email]!.availableSecrets
expect(Object.keys(newSecrets).length).toBe(2)
const key = Object.keys(newSecrets).find((k) => newSecrets[k]?.type === 'keyStore')
expect(key).toBeTruthy()
Expand All @@ -111,9 +111,9 @@ describe('happy cases', () => {
test('recoverKeyStore', async () => {
const ev = new EmailVaultController(storageCtrl, fetch, relayerUrl, keystore, testingOptions)
await ev.getEmailVaultInfo(email)
expect(Object.keys(ev.emailVaultStates.email[email].availableSecrets).length).toBe(1)
expect(Object.keys(ev.emailVaultStates.email[email]!.availableSecrets).length).toBe(1)
await ev.uploadKeyStoreSecret(email)
expect(Object.keys(ev.emailVaultStates.email[email].availableSecrets).length).toBe(2)
expect(Object.keys(ev.emailVaultStates.email[email]!.availableSecrets).length).toBe(2)

expect(keystore.isUnlocked).toBeFalsy()
await ev.recoverKeyStore(email, 'new_password')
Expand Down Expand Up @@ -182,10 +182,10 @@ describe('happy cases', () => {
email,
keys.map((k) => k.address)
)
expect(ev2.emailVaultStates.email[email].operations.length).toBe(2)
expect(ev2.emailVaultStates.email[email]!.operations.length).toBe(2)

await ev.fulfillSyncRequests(email, 'password')
expect(ev.emailVaultStates.email[email].operations.length).toBe(2)
expect(ev.emailVaultStates.email[email]!.operations.length).toBe(2)
await ev2.finalizeSyncKeys(
email,
keys.map((k) => k.address),
Expand All @@ -205,6 +205,37 @@ describe('happy cases', () => {
done()
}, 4000)

ev.handleMagicLinkKey(email, () => console.log('ready'))
void ev.handleMagicLinkKey(email, () => console.log('ready'))
})
test('remove keyStoreSecret', async () => {
const ev = new EmailVaultController(storageCtrl, fetch, relayerUrl, keystore, testingOptions)
await ev.getEmailVaultInfo(email)
await ev.uploadKeyStoreSecret(email)
expect(Object.keys(ev.emailVaultStates.email[email]!.availableSecrets).length).toBe(2)

// hacky way to get the secret so we can make sure the keystore cannot be decoded
// later with that secret
const uid = await keystore.getKeyStoreUid()
const evLib = new EmailVault(fetch, relayerUrl)
const key = ev.getMagicLinkKeyByEmail(email)?.key || ''
const recoverySecret = await evLib.retrieveKeyStoreSecret(email, key, uid)

await ev.removeKeyStoreSecret(email)
expect(Object.keys(ev.emailVaultStates.email[email]!.availableSecrets).length).toBe(1)
const remainingSecret = Object.values(ev.emailVaultStates.email[email]!.availableSecrets)[0]
expect(remainingSecret?.type).toBe('recoveryKey')

// attempt to unlock keystore with previous secret
await keystore.unlockWithSecret('EmailVaultRecoverySecret', recoverySecret.value!)
expect(keystore.isUnlocked).toBeFalsy()

expect(ev.keystoreRecoveryEmail).toBeFalsy()
expect(ev.hasKeystoreRecovery).toBeFalsy()
})
test('remove non-existing keyStoreSecret', async () => {
const ev = new EmailVaultController(storageCtrl, fetch, relayerUrl, keystore, testingOptions)
await ev.getEmailVaultInfo(email)
await ev.removeKeyStoreSecret(email)
expect(ev.emittedErrors.length).toBeGreaterThan(0)
})
})
80 changes: 69 additions & 11 deletions src/controllers/emailVault/emailVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum EmailVaultState {
Loading = 'loading',
WaitingEmailConfirmation = 'WaitingEmailConfirmation',
UploadingSecret = 'UploadingSecret',
RemovingSecret = 'RemovingSecret',
Ready = 'Ready'
}

Expand Down Expand Up @@ -57,6 +58,7 @@ function base64UrlEncode(str: string) {
const STATUS_WRAPPED_METHODS = {
getEmailVaultInfo: 'INITIAL',
uploadKeyStoreSecret: 'INITIAL',
removeKeyStoreSecret: 'INITIAL',
recoverKeyStore: 'INITIAL',
requestKeysSync: 'INITIAL',
finalizeSyncKeys: 'INITIAL'
Expand All @@ -80,6 +82,8 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon

#isUploadingSecret: boolean = false

#isRemovingSecret: boolean = false

#emailVault: EmailVault

#magicLinkKeys: MagicLinkKeys = {}
Expand Down Expand Up @@ -160,6 +164,7 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
if (!this.isReady) return EmailVaultState.Loading
if (this.#isWaitingEmailConfirmation) return EmailVaultState.WaitingEmailConfirmation
if (this.#isUploadingSecret) return EmailVaultState.UploadingSecret
if (this.#isRemovingSecret) return EmailVaultState.RemovingSecret

return EmailVaultState.Ready
}
Expand Down Expand Up @@ -257,8 +262,8 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
confirmed: true
}
fn && (await fn())
this.#storage.set(MAGIC_LINK_STORAGE_KEY, this.#magicLinkKeys)
this.#requestSessionKey(email)
void this.#storage.set(MAGIC_LINK_STORAGE_KEY, this.#magicLinkKeys)
void this.#requestSessionKey(email)
} else {
const code = classifyEmailVaultError(ev?.error)
const message = friendlyEmailVaultMessage(code, email)
Expand All @@ -276,7 +281,7 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon

async #getSessionKey(email: string): Promise<string | null> {
await this.#initialLoadPromise
return this.#sessionKeys[email]
return this.#sessionKeys[email] || null
}

getMagicLinkKeyByEmail(email: string): MagicLinkKey | null {
Expand Down Expand Up @@ -400,6 +405,55 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
this.emitUpdate()
}

async removeKeyStoreSecret(email: string) {
await this.withStatus('removeKeyStoreSecret', () => this.#removeKeyStoreSecret(email))
}

async #removeKeyStoreSecret(email: string) {
if (!this.emailVaultStates.email[email]) {
await this.#getEmailVaultInfo(email)
}

let result: Boolean | null = false
let magicKey = await this.#getMagicLinkKey(email)

if (!magicKey?.key && !this.#shouldStopConfirmationPolling) {
await this.handleMagicLinkKey(email, async () => {
magicKey = await this.#getMagicLinkKey(email)
})
}

if (magicKey?.key) {
this.#isRemovingSecret = true
const keyStoreUid = await this.#keyStore.getKeyStoreUid()
result = await this.#emailVault.removeKeyStoreSecretFromRelayer(
email,
magicKey.key,
keyStoreUid
)
await this.#keyStore.removeSecret(RECOVERY_SECRET_ID)
} else
this.emitError({
message: 'Email key not confirmed',
level: 'minor',
sendCrashReport: false,
error: new Error('removeKeyStoreSecret: not confirmed magic link key')
})

if (result) {
await this.#getEmailVaultInfo(email, 'setup')
} else {
this.emitError({
level: 'minor',
message: 'Error upload keyStore to email vault',
error: new Error('error removing keyStore secret from email vault')
})
}

this.#isRemovingSecret = false
this.emitUpdate()
}

async recoverKeyStore(email: string, newPassword: string) {
await this.withStatus('recoverKeyStore', () => this.#recoverKeyStore(email, newPassword))
}
Expand Down Expand Up @@ -503,7 +557,7 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
requester: keyStoreUid,
key
}))
if (magicLinkKey) {
if (magicLinkKey && this.emailVaultStates.email[email]) {
const newOperations = (await this.#emailVault.operations(
email,
magicLinkKey.key,
Expand Down Expand Up @@ -533,12 +587,13 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
level: 'major',
error: new Error("Can't pull operations")
})
return
}

// Promise.all makes race conditions
for (let i = 0; i < cloudOperations!.length; i++) {
const op = cloudOperations![i]
if (op.type === 'requestKeySync' && op.value) {
for (let i = 0; i < cloudOperations.length; i++) {
const op = cloudOperations[i]
if (op && op.type === 'requestKeySync' && op.value) {
const { privateKey } = JSON.parse(op.value || '{}')
await this.#keyStore.importKeyWithPublicKeyEncryption(privateKey, true)
}
Expand All @@ -552,7 +607,9 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
async finalizeSyncKeys(email: string, keys: string[], password: string) {
const operations: any[] = keys
.map((key) => {
const res = this.emailVaultStates.email[email].operations.find((op) => op.key === key)
const res = (this.emailVaultStates.email[email]?.operations || []).find(
(op) => op.key === key
)
if (!res) {
this.emitError({
message: `No sync request for key ${key}`,
Expand All @@ -574,12 +631,12 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon
// @TODO add password
async fulfillSyncRequests(email: string, password: string) {
await this.#getEmailVaultInfo(email)
const operations = this.emailVaultStates.email[email].operations
const operations = this.emailVaultStates.email[email]?.operations
const key = (await this.#getMagicLinkKey(email))?.key || (await this.#getSessionKey(email))
if (key) {
// pull keys from keystore for every operation
const newOperations: EmailVaultOperation[] = await Promise.all(
operations.map(async (op): Promise<EmailVaultOperation> => {
(operations || []).map(async (op): Promise<EmailVaultOperation> => {
if (op.type === 'requestKeySync') {
return {
...op,
Expand Down Expand Up @@ -635,8 +692,9 @@ export class EmailVaultController extends EventEmitter implements IEmailVaultCon

return EVEmails.find((email) => {
return (
this.emailVaultStates.email[email] &&
this.emailVaultStates.email[email].availableSecrets[keyStoreUid]?.type ===
SecretType.KeyStore
SecretType.KeyStore
)
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/emailVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type IEmailVaultController = ControllerInterface<
InstanceType<typeof import('../controllers/emailVault/emailVault').EmailVaultController>
>

export type MagicLinkFlow = 'recovery' | 'setup'
export type MagicLinkFlow = 'recovery' | 'setup' | 'removeSecret'

export enum SecretType {
RecoveryKey = 'recoveryKey',
Expand Down
19 changes: 19 additions & 0 deletions src/libs/emailVault/emailVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,25 @@ describe('err cases', () => {
)
})
})
describe('removeKeyStoreSecret', () => {
beforeEach(async () => {
await emailVault.getEmailVaultInfo(email, authKey)
})

test('add and remove keyStoreSecret', async () => {
const keyStoreUid = Wallet.createRandom().address
await emailVault.addKeyStoreSecret(email, authKey, keyStoreUid, keyStoreSecret)
const success = await emailVault.removeKeyStoreSecret(email, authKey, keyStoreUid)
expect(success).toBeTruthy()
})

test('remove non-existing keyStoreSecret', async () => {
const keyStoreUid = Wallet.createRandom().address
await expect(
emailVault.removeKeyStoreSecret(email, authKey, keyStoreUid)
).rejects.toHaveProperty(['output', 'res', 'message'], 'Error, missing KeyStore secret')
})
})
describe('retrieveKeyStoreSecret', () => {
beforeEach(async () => {
await emailVault.getEmailVaultInfo(email, authKey)
Expand Down
12 changes: 12 additions & 0 deletions src/libs/emailVault/emailVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ export class EmailVault {
).success
}

async removeKeyStoreSecretFromRelayer(
email: String,
authKey: String,
keyStoreUid: String
): Promise<Boolean> {
return (
await this.callRelayer(`/email-vault/remove-key-store-secret/${email}/${authKey}`, 'POST', {
uid: keyStoreUid
})
).success
}

async retrieveKeyStoreSecret(
email: String,
authKey: String,
Expand Down
Loading