fix(wallet): disclose backup passphrase only when needed (#127)
This commit is contained in:
@@ -50,6 +50,7 @@ const showRevealModal = ref(false)
|
|||||||
const revealPassword = ref('')
|
const revealPassword = ref('')
|
||||||
const revealCode = ref('')
|
const revealCode = ref('')
|
||||||
const revealPassphrase = ref('')
|
const revealPassphrase = ref('')
|
||||||
|
const showRevealPassphrase = ref(false)
|
||||||
const revealing = ref(false)
|
const revealing = ref(false)
|
||||||
const revealError = ref('')
|
const revealError = ref('')
|
||||||
const revealedWords = ref<string[]>([])
|
const revealedWords = ref<string[]>([])
|
||||||
@@ -60,6 +61,7 @@ function openReveal() {
|
|||||||
revealPassword.value = ''
|
revealPassword.value = ''
|
||||||
revealCode.value = ''
|
revealCode.value = ''
|
||||||
revealPassphrase.value = ''
|
revealPassphrase.value = ''
|
||||||
|
showRevealPassphrase.value = false
|
||||||
revealError.value = ''
|
revealError.value = ''
|
||||||
revealedWords.value = []
|
revealedWords.value = []
|
||||||
showRevealModal.value = true
|
showRevealModal.value = true
|
||||||
@@ -83,7 +85,17 @@ async function submitReveal() {
|
|||||||
// to set up a backup that now exists.
|
// to set up a backup that now exists.
|
||||||
void loadStatus()
|
void loadStatus()
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
revealError.value = e instanceof Error ? e.message : 'Failed to reveal the ecash phrase'
|
const message = e instanceof Error ? e.message : 'Failed to reveal the ecash phrase'
|
||||||
|
// Most operators used their login password as the backup passphrase. Do
|
||||||
|
// not confront everyone with an unexplained third credential up front;
|
||||||
|
// disclose it only when the authenticated password could not decrypt the
|
||||||
|
// node seed and a distinct setup-time passphrase may actually exist.
|
||||||
|
if (!status.value?.active && /could not decrypt the saved seed/i.test(message)) {
|
||||||
|
showRevealPassphrase.value = true
|
||||||
|
revealError.value = 'Your login password did not unlock the saved seed. Enter the separate backup passphrase you chose during setup.'
|
||||||
|
} else {
|
||||||
|
revealError.value = message
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
revealing.value = false
|
revealing.value = false
|
||||||
}
|
}
|
||||||
@@ -95,6 +107,7 @@ function closeReveal() {
|
|||||||
revealPassword.value = ''
|
revealPassword.value = ''
|
||||||
revealCode.value = ''
|
revealCode.value = ''
|
||||||
revealPassphrase.value = ''
|
revealPassphrase.value = ''
|
||||||
|
showRevealPassphrase.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyRevealedWords() {
|
async function copyRevealedWords() {
|
||||||
@@ -376,9 +389,9 @@ async function restoreFromPhrase() {
|
|||||||
<label class="block text-xs text-white/60 mb-1">2FA code <span class="text-white/30">(if enabled)</span></label>
|
<label class="block text-xs text-white/60 mb-1">2FA code <span class="text-white/30">(if enabled)</span></label>
|
||||||
<input v-model="revealCode" inputmode="numeric" autocomplete="one-time-code" class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm font-mono tracking-widest focus:outline-none focus:border-white/30" placeholder="123456" />
|
<input v-model="revealCode" inputmode="numeric" autocomplete="one-time-code" class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm font-mono tracking-widest focus:outline-none focus:border-white/30" placeholder="123456" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!status?.active">
|
<div v-if="showRevealPassphrase">
|
||||||
<label class="block text-xs text-white/60 mb-1">Backup passphrase <span class="text-white/30">(only if different from password)</span></label>
|
<label class="block text-xs text-white/60 mb-1">Separate backup passphrase</label>
|
||||||
<input v-model="revealPassphrase" type="password" class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:border-white/30" placeholder="Leave blank to use password" />
|
<input v-model="revealPassphrase" type="password" autocomplete="off" autofocus class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:border-white/30" placeholder="Passphrase chosen during setup" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="revealError" class="text-xs text-red-300 bg-red-500/10 border border-red-400/20 rounded-lg px-3 py-2">{{ revealError }}</p>
|
<p v-if="revealError" class="text-xs text-red-300 bg-red-500/10 border border-red-400/20 rounded-lg px-3 py-2">{{ revealError }}</p>
|
||||||
<div class="flex gap-2 pt-1">
|
<div class="flex gap-2 pt-1">
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
|
|
||||||
|
vi.mock('@/api/rpc-client', () => ({
|
||||||
|
rpcClient: { call: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
import EcashSeedBackup from '../EcashSeedBackup.vue'
|
||||||
|
|
||||||
|
let wrapper: VueWrapper | null = null
|
||||||
|
|
||||||
|
describe('EcashSeedBackup reveal credentials (#127)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper?.unmount()
|
||||||
|
wrapper = null
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
it('asks for a separate backup passphrase only after password decryption fails', async () => {
|
||||||
|
vi.mocked(rpcClient.call)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
active: false,
|
||||||
|
source: null,
|
||||||
|
can_activate: true,
|
||||||
|
derivable_from_node_seed: true,
|
||||||
|
})
|
||||||
|
.mockRejectedValueOnce(new Error(
|
||||||
|
'Could not decrypt the saved seed. If you set a separate backup passphrase during setup, enter that passphrase.',
|
||||||
|
))
|
||||||
|
|
||||||
|
wrapper = mount(EcashSeedBackup, { attachTo: document.body })
|
||||||
|
await flushPromises()
|
||||||
|
await wrapper.get('button').trigger('click')
|
||||||
|
|
||||||
|
expect(document.body.textContent).not.toContain('Separate backup passphrase')
|
||||||
|
const password = document.body.querySelector<HTMLInputElement>('input[autocomplete="current-password"]')!
|
||||||
|
password.value = 'login-password'
|
||||||
|
password.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
document.body.querySelector('form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(document.body.textContent).toContain('Separate backup passphrase')
|
||||||
|
expect(document.body.textContent).toContain('Your login password did not unlock the saved seed')
|
||||||
|
expect(document.body.querySelector('input[placeholder="Passphrase chosen during setup"]')).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user