Skip to content

Commit

Permalink
feat: vault migrations
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszjasiuk committed Dec 18, 2024
1 parent 8a88e6c commit cb1714d
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 114 deletions.
6 changes: 4 additions & 2 deletions apps/extension/src/background/vault/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ export class VaultService {
) {}

public async initialize(): Promise<void> {
const storage = await this.vaultStorage.get();
if (!storage) {
const exists = await this.vaultStorage.exists();
if (!exists) {
await this.vaultStorage.reset();
} else {
await this.vaultStorage.migrate();
}
}

Expand Down
4 changes: 2 additions & 2 deletions apps/extension/src/provider/data.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { KdfType } from "@namada/sdk/web";
import { AccountType, BridgeType, Chain, Extensions } from "@namada/types";
import { ActiveAccountStore } from "background/keyring";
import { Vault } from "background/vault";
import { VaultTypes } from "storage";
import { KeyStoreType } from "storage";

export const ACTIVE_ACCOUNT: ActiveAccountStore = {
id: "324bfe0e-cb19-5f1a-9630-9daaaecadabe",
Expand All @@ -30,7 +30,7 @@ export const chain: Chain = {
},
};

export const keyStore: Vault<VaultTypes>[] = [
export const keyStore: Vault<KeyStoreType>[] = [
{
public: {
alias: "Parent Account",
Expand Down
5 changes: 3 additions & 2 deletions apps/extension/src/storage/VaultStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AccountType } from "@namada/types";
import { KVStoreMock } from "test/init";
import { KeyStore, VaultStorage } from "./VaultStorage";
import { VaultStorage } from "./VaultStorage";
import { KeyStore } from "./schemas";

jest.mock("webextension-polyfill", () => ({}));

Expand Down Expand Up @@ -40,7 +41,7 @@ const addNonSensitiveData = async (
): Promise<void> => {
await vaultStorage.set({
data: {
test: [
"key-store": [
{
public: {
id: `${id}`,
Expand Down
135 changes: 27 additions & 108 deletions apps/extension/src/storage/VaultStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,116 +2,19 @@ import { KVStore } from "@namada/storage";
import * as E from "fp-ts/Either";
import * as t from "io-ts";
import { ExtStorage } from "./Storage";
import {
KeyStore,
KeyStoreType,
migrateVault,
SensitiveType,
Vault,
VaultType,
} from "./schemas";

export type PrimitiveType = string | number | boolean;

const Uint8ArrayCodec = new t.Type<Uint8Array, Uint8Array, unknown>(
"Uint8Array",
(u): u is Uint8Array => u instanceof Uint8Array,
(u, c) => {
try {
const a = Array.from(u as Uint8Array);
return t.success(Uint8Array.from(a));
} catch {
return t.failure(u, c);
}
},
t.identity
);

enum KdfType {
Argon2 = "argon2",
Scrypt = "scrypt",
}

export const Sensitive = t.type({
cipher: t.type({
type: t.literal("aes-256-gcm"),
iv: Uint8ArrayCodec,
text: Uint8ArrayCodec,
}),
kdf: t.type({
type: t.keyof({
[KdfType.Argon2]: null,
[KdfType.Scrypt]: null,
}),
params: t.any,
}),
});
export type SensitiveType = t.TypeOf<typeof Sensitive>;

enum AccountType {
Mnemonic = "mnemonic",
PrivateKey = "private-key",
ShieldedKeys = "shielded-keys",
Ledger = "ledger",
}

export const KeyStore = t.exact(
t.intersection([
t.type({
id: t.string,
alias: t.string,
address: t.string,
owner: t.string,
path: t.intersection([
t.type({
account: t.number,
}),
t.partial({
change: t.number,
index: t.number,
}),
]),
type: t.keyof({
[AccountType.Mnemonic]: null,
[AccountType.PrivateKey]: null,
[AccountType.ShieldedKeys]: null,
[AccountType.Ledger]: null,
}),
source: t.keyof({
imported: null,
generated: null,
}),
timestamp: t.number,
}),
t.partial({
publicKey: t.string,
parentId: t.string,
pseudoExtendedKey: t.string,
}),
])
);
export type KeyStoreType = t.TypeOf<typeof KeyStore>;

const Vault = t.intersection([
t.type({
data: t.record(
t.string,
t.array(
t.intersection([
t.type({
public: KeyStore,
}),
t.partial({
sensitive: Sensitive,
}),
])
)
),
}),
t.partial({
password: Sensitive,
}),
]);

type VaultType = t.TypeOf<typeof Vault>;

export type VaultTypes = KeyStoreType;

export type VaultSchemas = typeof KeyStore;

export type VaultKeys = "key-store";
export type VaultSchemas = typeof KeyStore;

export const schemasMap = new Map<VaultSchemas, VaultKeys>([
[KeyStore, "key-store"],
Expand All @@ -122,6 +25,12 @@ export class VaultStorage extends ExtStorage {
super(provider);
}

public async migrate(): Promise<void> {
const data = await this.getRaw("vault");
const newData = migrateVault(data);
await this.set(newData);
}

public validate(data: unknown): VaultType {
const decodedData = Vault.decode(data);

Expand All @@ -144,6 +53,11 @@ export class VaultStorage extends ExtStorage {
return decoded.right;
}

public async exists(): Promise<boolean> {
const data = await this.getRaw("vault");
return !this.isEmpty(data);
}

public async getOrFail(): Promise<VaultType> {
const storedData = await this.get();
if (!storedData) {
Expand Down Expand Up @@ -217,7 +131,7 @@ export class VaultStorage extends ExtStorage {
schema: S,
prop: keyof t.TypeOf<S>,
value: string,
newProps: Partial<VaultTypes>
newProps: Partial<KeyStoreType>
): Promise<t.TypeOf<S>> {
const accountIdx = await this.findIndexOrFail(schema, prop, value);
const storedData = await this.getSpecificOrFail(schema);
Expand All @@ -230,7 +144,7 @@ export class VaultStorage extends ExtStorage {

public async remove<S extends VaultSchemas>(
schema: S,
prop: keyof VaultTypes,
prop: keyof KeyStoreType,
value: PrimitiveType
): Promise<{ public: t.TypeOf<S> }[]> {
const storedData = (await this.getSpecific(schema)) || [];
Expand Down Expand Up @@ -334,4 +248,9 @@ export class VaultStorage extends ExtStorage {

return key;
}

private isEmpty(data: unknown): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Object.keys((data as any)?.data || {}).length === 0;
}
}
1 change: 1 addition & 0 deletions apps/extension/src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./LocalStorage";
export * from "./schemas";
export * from "./VaultStorage";
94 changes: 94 additions & 0 deletions apps/extension/src/storage/schemas/VaultSchemaV1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as t from "io-ts";

const Uint8ArrayCodec = new t.Type<Uint8Array, Uint8Array, unknown>(
"Uint8Array",
(u): u is Uint8Array => u instanceof Uint8Array,
(u, c) => {
try {
const a = Array.from(u as Uint8Array);
return t.success(Uint8Array.from(a));
} catch {
return t.failure(u, c);
}
},
t.identity
);

enum KdfType {
Argon2 = "argon2",
Scrypt = "scrypt",
}

export const Sensitive = t.type({
cipher: t.type({
type: t.literal("aes-256-gcm"),
iv: Uint8ArrayCodec,
text: Uint8ArrayCodec,
}),
kdf: t.type({
type: t.keyof({
[KdfType.Argon2]: null,
[KdfType.Scrypt]: null,
}),
params: t.any,
}),
});

enum AccountType {
Mnemonic = "mnemonic",
PrivateKey = "private-key",
ShieldedKeys = "shielded-keys",
Ledger = "ledger",
}

export const KeyStore = t.exact(
t.intersection([
t.type({
id: t.string,
alias: t.string,
address: t.string,
owner: t.string,
path: t.intersection([
t.type({
account: t.number,
}),
t.partial({
change: t.number,
index: t.number,
}),
]),
type: t.keyof({
[AccountType.Mnemonic]: null,
[AccountType.PrivateKey]: null,
[AccountType.ShieldedKeys]: null,
[AccountType.Ledger]: null,
}),
}),
t.partial({
publicKey: t.string,
parentId: t.string,
pseudoExtendedKey: t.string,
}),
])
);

export const Vault = t.intersection([
t.type({
data: t.record(
t.string,
t.array(
t.intersection([
t.type({
public: KeyStore,
}),
t.partial({
sensitive: Sensitive,
}),
])
)
),
}),
t.partial({
password: Sensitive,
}),
]);
Loading

0 comments on commit cb1714d

Please sign in to comment.