flotilla/packages/desktop-secure-storage/test/store.test.mjs
2026-09-22 16:32:08 +00:00

197 lines
6.3 KiB
JavaScript

import {open, mkdtemp, readFile, rename, rm, stat, unlink, writeFile} from "node:fs/promises"
import {tmpdir} from "node:os"
import {join} from "node:path"
import {afterEach, describe, it} from "node:test"
import assert from "node:assert/strict"
import {createEncryptedStore} from "../electron/dist/store.mjs"
const directories = []
const makeDirectory = async () => {
const directory = await mkdtemp(join(tmpdir(), "flotilla-secure-storage-"))
directories.push(directory)
return directory
}
const makeCrypto = ({reEncrypt = false} = {}) => ({
encrypt: async value => Buffer.from(`enc:${Buffer.from(value).toString("base64")}`),
decrypt: async value => ({
result: Buffer.from(value.toString().slice(4), "base64").toString(),
shouldReEncrypt: reEncrypt,
}),
})
const makeStore = async (options = {}) => {
const directory = await makeDirectory()
const crypto = makeCrypto(options)
const store = createEncryptedStore({
filePath: join(directory, "secure-storage.bin"),
...crypto,
...options,
})
await store.ready
return {directory, store}
}
afterEach(async () => {
await Promise.all(
directories.splice(0).map(directory => rm(directory, {recursive: true, force: true})),
)
})
describe("encrypted desktop store", () => {
it("starts with an empty map when the encrypted file is missing", async () => {
const {store} = await makeStore()
assert.equal(await store.get("session"), undefined)
})
it("persists values without plaintext", async () => {
const {directory, store} = await makeStore()
await store.set("session", "fixture-secret")
assert.equal(await store.get("session"), "fixture-secret")
const file = await readFile(join(directory, "secure-storage.bin"))
assert.equal(file.includes("fixture-secret"), false)
if (process.platform !== "win32") {
assert.equal((await stat(join(directory, "secure-storage.bin"))).mode & 0o777, 0o600)
}
})
it("serializes concurrent writes", async () => {
const {directory, store} = await makeStore()
await Promise.all([
store.set("session", "a"),
store.set("wallet", "b"),
store.set("sessions", "c"),
])
const reopened = createEncryptedStore({
filePath: join(directory, "secure-storage.bin"),
...makeCrypto(),
})
await reopened.ready
assert.deepEqual(
await Promise.all([
reopened.get("session"),
reopened.get("wallet"),
reopened.get("sessions"),
]),
["a", "b", "c"],
)
})
it("preserves empty string values", async () => {
const {store} = await makeStore()
await store.set("empty", "")
assert.equal(await store.get("empty"), "")
})
it("removes individual values and clears the encrypted file", async () => {
const {directory, store} = await makeStore()
await store.set("session", "fixture-secret")
await store.set("wallet", "wallet-secret")
await store.remove("session")
assert.equal(await store.get("session"), undefined)
assert.equal(await store.get("wallet"), "wallet-secret")
await store.clear()
assert.equal(await store.get("wallet"), undefined)
await assert.rejects(readFile(join(directory, "secure-storage.bin")), {code: "ENOENT"})
})
it("leaves the previous file when a replacement fails", async () => {
const {directory, store} = await makeStore()
await store.set("session", "before")
const failing = createEncryptedStore({
filePath: join(directory, "secure-storage.bin"),
...makeCrypto(),
fs: {
open,
readFile,
rename: async () => {
throw new Error("replacement failed")
},
unlink: async () => undefined,
writeFile,
},
})
await failing.ready
await assert.rejects(failing.set("session", "after"), /replacement failed/)
const reopened = createEncryptedStore({
filePath: join(directory, "secure-storage.bin"),
...makeCrypto(),
})
await reopened.ready
assert.equal(await reopened.get("session"), "before")
})
it("keeps the previous file when flushing the replacement fails", async () => {
const {directory, store} = await makeStore()
await store.set("session", "before")
const filePath = join(directory, "secure-storage.bin")
const original = await readFile(filePath)
const failing = createEncryptedStore({
filePath,
...makeCrypto(),
fs: {
readFile,
rename,
unlink,
open: async (...args) => {
const file = await open(...args)
return {
writeFile: value => file.writeFile(value),
sync: async () => {
throw new Error("flush failed")
},
close: () => file.close(),
}
},
},
})
await assert.rejects(failing.set("session", "after"), /flush failed/)
assert.deepEqual(await readFile(filePath), original)
assert.equal(await failing.get("session"), "before")
})
it("rewrites values when the key provider requests re-encryption", async () => {
const {directory, store} = await makeStore()
await store.set("session", "fixture-secret")
let writes = 0
const reopened = createEncryptedStore({
filePath: join(directory, "secure-storage.bin"),
...makeCrypto({reEncrypt: true}),
fs: {
open,
readFile,
unlink,
rename: async (...args) => {
writes += 1
return rename(...args)
},
},
})
await reopened.ready
assert.equal(await reopened.get("session"), "fixture-secret")
assert.equal(writes, 1)
})
it("rejects corrupt encrypted data", async () => {
const directory = await makeDirectory()
const filePath = join(directory, "secure-storage.bin")
await writeFile(filePath, "corrupt")
const store = createEncryptedStore({filePath, ...makeCrypto()})
await assert.rejects(store.ready, /Encrypted desktop storage is invalid/)
})
it("rejects undecryptable encrypted data", async () => {
const directory = await makeDirectory()
const filePath = join(directory, "secure-storage.bin")
await writeFile(filePath, "encrypted")
const store = createEncryptedStore({
filePath,
encrypt: async value => Buffer.from(value),
decrypt: async () => {
throw new Error("decrypt failed")
},
})
await assert.rejects(store.ready, /decrypt failed/)
})
})