mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-24 21:40:48 +02:00
249 lines
8.8 KiB
TypeScript
249 lines
8.8 KiB
TypeScript
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { expect } from '@wdio/globals'
|
|
import {
|
|
tauri,
|
|
tauriError,
|
|
describePlugin,
|
|
scratchDir
|
|
} from '../helpers/index.js'
|
|
|
|
// The example derives the snapshot key from the password with argon2. Each
|
|
// test works on its own snapshot file under the spec's scratch directory,
|
|
// which the fs plugin cleans up; the snapshot path is resolved inside the page.
|
|
|
|
const dir = scratchDir('stronghold')
|
|
const password = 'e2e-password'
|
|
const clientName = 'e2e-client'
|
|
|
|
/** An arbitrary, valid BIP39 mnemonic, so derived keys are deterministic. */
|
|
const mnemonic =
|
|
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
|
|
|
describePlugin('stronghold', () => {
|
|
before(async () => {
|
|
await tauri(async (api, dir) => {
|
|
const baseDir = api.fs.BaseDirectory.AppData
|
|
if (await api.fs.exists(dir, { baseDir })) {
|
|
await api.fs.remove(dir, { baseDir, recursive: true })
|
|
}
|
|
await api.fs.mkdir(dir, { baseDir, recursive: true })
|
|
}, dir)
|
|
})
|
|
|
|
after(async () => {
|
|
await tauri(async (api, dir) => {
|
|
const baseDir = api.fs.BaseDirectory.AppData
|
|
if (await api.fs.exists(dir, { baseDir })) {
|
|
await api.fs.remove(dir, { baseDir, recursive: true })
|
|
}
|
|
}, dir)
|
|
})
|
|
|
|
it('store records round-trip and persist in the snapshot', async () => {
|
|
const result = await tauri(
|
|
async (api, dir, password, clientName) => {
|
|
const path = await api.path.join(
|
|
await api.path.appDataDir(),
|
|
dir,
|
|
'store.stronghold'
|
|
)
|
|
const value = Array.from(new TextEncoder().encode('top secret'))
|
|
|
|
const stronghold = await api.stronghold.Stronghold.load(path, password)
|
|
const client = await stronghold.createClient(clientName)
|
|
await client.getStore().insert('key', value)
|
|
const sameSession = await client.getStore().get('key')
|
|
await stronghold.save()
|
|
await stronghold.unload()
|
|
|
|
const reopened = await api.stronghold.Stronghold.load(path, password)
|
|
const store = (await reopened.loadClient(clientName)).getStore()
|
|
const persisted = await store.get('key')
|
|
const missing = await store.get('missing')
|
|
await reopened.unload()
|
|
|
|
return {
|
|
sameSession: sameSession && Array.from(sameSession),
|
|
persisted: persisted && new TextDecoder().decode(persisted),
|
|
missing,
|
|
fileExists: await api.fs.exists(path)
|
|
}
|
|
},
|
|
dir,
|
|
password,
|
|
clientName
|
|
)
|
|
expect(result.sameSession).toEqual(
|
|
Array.from(new TextEncoder().encode('top secret'))
|
|
)
|
|
expect(result.persisted).toBe('top secret')
|
|
expect(result.missing).toBeNull()
|
|
expect(result.fileExists).toBe(true)
|
|
})
|
|
|
|
it('store.remove returns the removed value', async () => {
|
|
const result = await tauri(
|
|
async (api, dir, password, clientName) => {
|
|
const path = await api.path.join(
|
|
await api.path.appDataDir(),
|
|
dir,
|
|
'remove.stronghold'
|
|
)
|
|
const stronghold = await api.stronghold.Stronghold.load(path, password)
|
|
const store = (await stronghold.createClient(clientName)).getStore()
|
|
await store.insert('key', [1, 2, 3])
|
|
const removed = await store.remove('key')
|
|
const after = await store.get('key')
|
|
await stronghold.unload()
|
|
return { removed: removed && Array.from(removed), after }
|
|
},
|
|
dir,
|
|
password,
|
|
clientName
|
|
)
|
|
expect(result).toEqual({ removed: [1, 2, 3], after: null })
|
|
})
|
|
|
|
it('a snapshot cannot be opened with the wrong password', async () => {
|
|
const path = await tauri(
|
|
async (api, dir, password, clientName) => {
|
|
const path = await api.path.join(
|
|
await api.path.appDataDir(),
|
|
dir,
|
|
'password.stronghold'
|
|
)
|
|
const stronghold = await api.stronghold.Stronghold.load(path, password)
|
|
await stronghold.createClient(clientName)
|
|
await stronghold.unload()
|
|
return path
|
|
},
|
|
dir,
|
|
password,
|
|
clientName
|
|
)
|
|
const error = await tauriError(
|
|
(api, path) => api.stronghold.Stronghold.load(path, 'wrong-password'),
|
|
path
|
|
)
|
|
expect(error).toMatch(/failed to decode\/decrypt/)
|
|
})
|
|
|
|
it('loading a client that was never created is rejected', async () => {
|
|
const error = await tauriError(
|
|
async (api, dir, password) => {
|
|
const path = await api.path.join(
|
|
await api.path.appDataDir(),
|
|
dir,
|
|
'no-client.stronghold'
|
|
)
|
|
const stronghold = await api.stronghold.Stronghold.load(path, password)
|
|
try {
|
|
await stronghold.loadClient('never-created')
|
|
} finally {
|
|
await stronghold.unload()
|
|
}
|
|
},
|
|
dir,
|
|
password
|
|
)
|
|
expect(error).toMatch(/error loading client data/)
|
|
})
|
|
|
|
it('vault procedures derive keys and sign without exposing secrets', async () => {
|
|
const result = await tauri(
|
|
async (api, dir, password, clientName, mnemonic) => {
|
|
const { Location } = api.stronghold
|
|
const path = await api.path.join(
|
|
await api.path.appDataDir(),
|
|
dir,
|
|
'vault.stronghold'
|
|
)
|
|
const stronghold = await api.stronghold.Stronghold.load(path, password)
|
|
const vault = (await stronghold.createClient(clientName)).getVault(
|
|
'vault'
|
|
)
|
|
|
|
// the same mnemonic recovered twice derives the same key
|
|
const seedA = Location.generic('vault', 'seed-a')
|
|
const seedB = Location.generic('vault', 'seed-b')
|
|
await vault.recoverBIP39(mnemonic, seedA)
|
|
await vault.recoverBIP39(mnemonic, seedB)
|
|
// Ed25519 SLIP-10 only derives hardened indices (the high bit set).
|
|
// (No named helper: the transpiler would wrap it in a `__name` call
|
|
// that does not exist in the page.)
|
|
const chain = [44, 4218, 0, 0, 0].map((i) => (i | 0x80000000) >>> 0)
|
|
const otherChain = [44, 4218, 0, 0, 1].map(
|
|
(i) => (i | 0x80000000) >>> 0
|
|
)
|
|
const keyA = Location.generic('vault', 'key-a')
|
|
const keyB = Location.generic('vault', 'key-b')
|
|
const keyOther = Location.generic('vault', 'key-other')
|
|
await vault.deriveSLIP10(chain, 'Seed', seedA, keyA)
|
|
await vault.deriveSLIP10(chain, 'Seed', seedB, keyB)
|
|
await vault.deriveSLIP10(otherChain, 'Seed', seedA, keyOther)
|
|
|
|
const publicA = Array.from(await vault.getEd25519PublicKey(keyA))
|
|
const publicB = Array.from(await vault.getEd25519PublicKey(keyB))
|
|
const publicOther = Array.from(
|
|
await vault.getEd25519PublicKey(keyOther)
|
|
)
|
|
const signature = Array.from(await vault.signEd25519(keyA, 'message'))
|
|
const signatureAgain = Array.from(
|
|
await vault.signEd25519(keyA, 'message')
|
|
)
|
|
|
|
// a random seed and a raw secret can be stored and removed
|
|
const random = Location.generic('vault', 'random')
|
|
await vault.generateSLIP10Seed(random)
|
|
await vault.insert('raw', [9, 9, 9])
|
|
await vault.remove(Location.generic('vault', 'raw'))
|
|
|
|
// a generated mnemonic recovers the seed generateBIP39 stored
|
|
const generatedSeed = Location.generic('vault', 'generated')
|
|
const mnemonicBytes = await vault.generateBIP39(generatedSeed)
|
|
const recoveredSeed = Location.generic('vault', 'recovered')
|
|
await vault.recoverBIP39(
|
|
new TextDecoder().decode(mnemonicBytes),
|
|
recoveredSeed
|
|
)
|
|
const generatedKey = Location.generic('vault', 'generated-key')
|
|
const recoveredKey = Location.generic('vault', 'recovered-key')
|
|
await vault.deriveSLIP10(chain, 'Seed', generatedSeed, generatedKey)
|
|
await vault.deriveSLIP10(chain, 'Seed', recoveredSeed, recoveredKey)
|
|
const generatedPublic = Array.from(
|
|
await vault.getEd25519PublicKey(generatedKey)
|
|
)
|
|
const recoveredPublic = Array.from(
|
|
await vault.getEd25519PublicKey(recoveredKey)
|
|
)
|
|
|
|
await stronghold.unload()
|
|
return {
|
|
publicA,
|
|
publicB,
|
|
publicOther,
|
|
signature,
|
|
signatureAgain,
|
|
generatedPublic,
|
|
recoveredPublic
|
|
}
|
|
},
|
|
dir,
|
|
password,
|
|
clientName,
|
|
mnemonic
|
|
)
|
|
expect(result.publicA).toHaveLength(32)
|
|
expect(result.publicB).toEqual(result.publicA)
|
|
expect(result.publicOther).not.toEqual(result.publicA)
|
|
// Ed25519 signatures are 64 bytes and deterministic
|
|
expect(result.signature).toHaveLength(64)
|
|
expect(result.signatureAgain).toEqual(result.signature)
|
|
expect(result.recoveredPublic).toEqual(result.generatedPublic)
|
|
expect(result.generatedPublic).not.toEqual(result.publicA)
|
|
})
|
|
})
|