Skip to content

Commit

Permalink
web-wallet: Secure seed with ephemeral encryption
Browse files Browse the repository at this point in the history
Resolves #3354
  • Loading branch information
ascartabelli committed Jan 15, 2025
1 parent e3bd946 commit 2ff97cd
Show file tree
Hide file tree
Showing 18 changed files with 181 additions and 114 deletions.
3 changes: 3 additions & 0 deletions web-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add ephemeral encryption to secure the seed while in memory [#3354]

### Changed

- Update Transactions list design [#1922]
Expand Down Expand Up @@ -522,6 +524,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#3287]: https://github.com/dusk-network/rusk/issues/3287
[#3333]: https://github.com/dusk-network/rusk/issues/3333
[#3339]: https://github.com/dusk-network/rusk/issues/3339
[#3354]: https://github.com/dusk-network/rusk/issues/3354
[#3356]: https://github.com/dusk-network/rusk/issues/3356

<!-- VERSIONS -->
Expand Down
4 changes: 2 additions & 2 deletions web-wallet/src/lib/services/loginInfoStorage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const fromStorageString = unless(
const toStorageString = compose(JSON.stringify, mapValuesWith(bytesToBase64));

const loginInfoStorage = {
/** @returns {MnemonicEncryptInfo | null} */
/** @returns {WalletEncryptInfo | null} */
get() {
return fromStorageString(localStorage.getItem(storeKey));
},
Expand All @@ -19,7 +19,7 @@ const loginInfoStorage = {
localStorage.removeItem(storeKey);
},

/** @param {MnemonicEncryptInfo} info */
/** @param {WalletEncryptInfo} info */
set(info) {
localStorage.setItem(storeKey, toStorageString(info));
},
Expand Down
16 changes: 16 additions & 0 deletions web-wallet/src/lib/wallet/__tests__/decryptBuffer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { generateMnemonic } from "bip39";

import { decryptBuffer, encryptBuffer } from "..";

describe("decryptBuffer", () => {
const plaintext = new TextEncoder().encode(generateMnemonic());
const pwd = "some password";

it("should be able to decrypt the mnemonic phrase using the given password", async () => {
const encryptInfo = await encryptBuffer(plaintext, pwd);
const decrypted = await decryptBuffer(encryptInfo, pwd);

expect(decrypted.toString()).toBe(plaintext.buffer.toString());
});
});
21 changes: 21 additions & 0 deletions web-wallet/src/lib/wallet/__tests__/encryptBuffer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { generateMnemonic } from "bip39";

import { encryptBuffer, getSeedFromMnemonic } from "..";

describe("encryptBuffer", () => {
it("should be able to encrypt a buffer using the given password", async () => {
const pwd = "some password";
const buffer = getSeedFromMnemonic(generateMnemonic());
const result = await encryptBuffer(buffer, pwd);

expect(result).toMatchObject({
data: expect.any(Uint8Array),
iv: expect.any(Uint8Array),
salt: expect.any(Uint8Array),
});
expect(result.data.toString()).not.toBe(buffer.toString());
expect(result.iv.length).toBe(12);
expect(result.salt.length).toBe(32);
});
});
3 changes: 3 additions & 0 deletions web-wallet/src/lib/wallet/__tests__/encryptMnemonic.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe("encryptMnemonic", () => {
iv: expect.any(Uint8Array),
salt: expect.any(Uint8Array),
});
expect(result.data.toString()).not.toBe(
new TextEncoder().encode(mnemonic).toString()
);
expect(result.iv.length).toBe(12);
expect(result.salt.length).toBe(32);
});
Expand Down
11 changes: 7 additions & 4 deletions web-wallet/src/lib/wallet/__tests__/profileGeneratorFrom.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ describe("profileGeneratorFrom", () => {
const mnemonic =
"cart dad sail wreck robot grit combine noble rap farm slide sad";
const seed = getSeedFromMnemonic(mnemonic);
const seedCopy = seed.slice();

profileGeneratorFrom(seed);
await profileGeneratorFrom(seed);

const seederResult = ProfileGeneratorMock.mock.calls[0][0]();
const seederResult = await ProfileGeneratorMock.mock.calls[0][0]();

expect(ProfileGeneratorMock).toHaveBeenCalledTimes(1);
expect(seederResult).toBeInstanceOf(Promise);
await expect(seederResult).resolves.toBe(seed);
expect(seederResult.toString()).toBe(seed.toString());

// ensures that the function doesn't mutate the seed
expect(seed.toString()).toBe(seedCopy.toString());
});
});
15 changes: 15 additions & 0 deletions web-wallet/src/lib/wallet/decryptBuffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import getDerivedKey from "./getDerivedKey";

/**
* @param {WalletEncryptInfo} encryptInfo
* @param {string} pwd
* @returns {Promise<ArrayBuffer>}
*/
async function decryptBuffer(encryptInfo, pwd) {
const { data, iv, salt } = encryptInfo;
const key = await getDerivedKey(pwd, salt);

return await crypto.subtle.decrypt({ iv, name: "AES-GCM" }, key, data);
}

export default decryptBuffer;
21 changes: 6 additions & 15 deletions web-wallet/src/lib/wallet/decryptMnemonic.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import getDerivedKey from "./getDerivedKey";
import decryptBuffer from "./decryptBuffer";

/**
* @param {MnemonicEncryptInfo} mnemonicEncryptInfo
* @param {String} pwd
* @returns {Promise<String>}
* @param {WalletEncryptInfo} encryptInfo
* @param {string} pwd
* @returns {Promise<string>}
*/
async function decryptMnemonic(mnemonicEncryptInfo, pwd) {
const { data, iv, salt } = mnemonicEncryptInfo;
const key = await getDerivedKey(pwd, salt);
const plaintext = await crypto.subtle.decrypt(
{ iv, name: "AES-GCM" },
key,
data
);

return new TextDecoder().decode(plaintext);
}
const decryptMnemonic = async (encryptInfo, pwd) =>
new TextDecoder().decode(await decryptBuffer(encryptInfo, pwd));

export default decryptMnemonic;
19 changes: 19 additions & 0 deletions web-wallet/src/lib/wallet/encryptBuffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import getDerivedKey from "./getDerivedKey";

/**
* @param {BufferSource} buffer
* @param {string} pwd
* @returns {Promise<WalletEncryptInfo>}
*/
async function encryptBuffer(buffer, pwd) {
const salt = crypto.getRandomValues(new Uint8Array(32));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await getDerivedKey(pwd, salt);
const data = new Uint8Array(
await crypto.subtle.encrypt({ iv, name: "AES-GCM" }, key, buffer)
);

return { data, iv, salt };
}

export default encryptBuffer;
21 changes: 6 additions & 15 deletions web-wallet/src/lib/wallet/encryptMnemonic.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import getDerivedKey from "./getDerivedKey";
import encryptBuffer from "./encryptBuffer";

/**
* @param {String} mnemonic
* @param {String} pwd
* @returns {Promise<MnemonicEncryptInfo>}
* @param {string} mnemonic
* @param {string} pwd
* @returns {Promise<WalletEncryptInfo>}
*/
async function encryptMnemonic(mnemonic, pwd) {
const plaintext = new TextEncoder().encode(mnemonic);
const salt = crypto.getRandomValues(new Uint8Array(32));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await getDerivedKey(pwd, salt);
const data = new Uint8Array(
await crypto.subtle.encrypt({ iv, name: "AES-GCM" }, key, plaintext)
);

return { data, iv, salt };
}
const encryptMnemonic = async (mnemonic, pwd) =>
await encryptBuffer(new TextEncoder().encode(mnemonic), pwd);

export default encryptMnemonic;
2 changes: 2 additions & 0 deletions web-wallet/src/lib/wallet/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as decryptBuffer } from "./decryptBuffer";
export { default as decryptMnemonic } from "./decryptMnemonic";
export { default as encryptBuffer } from "./encryptBuffer";
export { default as encryptMnemonic } from "./encryptMnemonic";
export { default as getSeedFromMnemonic } from "./getSeedFromMnemonic";
export { default as initializeWallet } from "./initializeWallet";
Expand Down
8 changes: 5 additions & 3 deletions web-wallet/src/lib/wallet/initializeWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import profileGeneratorFrom from "./profileGeneratorFrom";

/**
* @param {string} mnemonic
* @param {bigint | undefined} syncFrom
* @param {bigint} [syncFrom]
*/
async function initializeWallet(mnemonic, syncFrom = undefined) {
async function initializeWallet(mnemonic, syncFrom) {
settingsStore.reset();

const profileGenerator = profileGeneratorFrom(getSeedFromMnemonic(mnemonic));
const profileGenerator = await profileGeneratorFrom(
getSeedFromMnemonic(mnemonic)
);

walletStore.clearLocalDataAndInit(profileGenerator, syncFrom);
}
Expand Down
26 changes: 18 additions & 8 deletions web-wallet/src/lib/wallet/profileGeneratorFrom.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { ProfileGenerator } from "$lib/vendor/w3sper.js/src/mod";

/** @type {(seed: Uint8Array) => ProfileGenerator} */
function profileGeneratorFrom(seed) {
/*
* For now we create a function that returns
* a constant value.
* In future we can add some encrypt / decrypt logic.
*/
const seeder = async () => seed;
import decryptBuffer from "./decryptBuffer";
import encryptBuffer from "./encryptBuffer";

/** @type {(seed: Uint8Array) => Promise<ProfileGenerator>} */
async function profileGeneratorFrom(seed) {
// creating a local copy
seed = seed.slice();

const pwd = new TextDecoder().decode(
crypto.getRandomValues(new Uint8Array(32))
);
const encryptInfo = await encryptBuffer(seed, pwd);

// destroying data inside the local copy
seed.fill(0);

const seeder = async () =>
new Uint8Array(await decryptBuffer(encryptInfo, pwd));

return new ProfileGenerator(seeder);
}
Expand Down
2 changes: 1 addition & 1 deletion web-wallet/src/lib/wallet/wallet.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type MnemonicEncryptInfo = {
type WalletEncryptInfo = {
data: Uint8Array;
iv: Uint8Array;
salt: Uint8Array;
Expand Down
Loading

0 comments on commit 2ff97cd

Please sign in to comment.