You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
8.6 KiB
243 lines
8.6 KiB
import BaseDexie, { Table } from "dexie";
|
|
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
|
import * as R from "ramda";
|
|
|
|
import { Account, AccountsSchema } from "./tables/accounts";
|
|
import { Contact, ContactSchema } from "./tables/contacts";
|
|
import { Log, LogSchema } from "./tables/logs";
|
|
import { MASTER_SECRET_KEY, Secret, SecretSchema } from "./tables/secret";
|
|
import {
|
|
MASTER_SETTINGS_KEY,
|
|
Settings,
|
|
SettingsSchema,
|
|
} from "./tables/settings";
|
|
import { Temp, TempSchema } from "./tables/temp";
|
|
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
|
import { logger } from "../utils/logger";
|
|
|
|
// Define types for tables that hold sensitive and non-sensitive data
|
|
type SecretTable = { secret: Table<Secret> };
|
|
type SensitiveTables = { accounts: Table<Account> };
|
|
type NonsensitiveTables = {
|
|
contacts: Table<Contact>;
|
|
logs: Table<Log>;
|
|
settings: Table<Settings>;
|
|
temp: Table<Temp>;
|
|
};
|
|
|
|
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
|
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
|
BaseDexie & T;
|
|
|
|
//// Initialize the DBs, starting with the sensitive ones.
|
|
|
|
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
|
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
|
secretDB.version(1).stores(SecretSchema);
|
|
|
|
// Initialize Dexie database for accounts
|
|
const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|
|
|
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
|
|
// so that it's clear whether the usage needs the private key inside.
|
|
//
|
|
// This is a promise because the decryption key comes from IndexedDB
|
|
// and someday it may come from a password or keystore or external wallet.
|
|
// It's important that usages take into account that there may be a delay due
|
|
// to a user action required to unlock the data.
|
|
export const accountsDBPromise = useSecretAndInitializeAccountsDB(
|
|
secretDB,
|
|
accountsDexie,
|
|
);
|
|
|
|
//// Now initialize the other DB.
|
|
|
|
// Initialize Dexie databases for non-sensitive data
|
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
|
|
|
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
|
|
|
|
// v1 also had contacts & settings
|
|
// v2 added Log
|
|
db.version(2).stores({
|
|
...ContactSchema,
|
|
...LogSchema,
|
|
...{ settings: "id" }, // old Settings schema
|
|
});
|
|
// v3 added Temp
|
|
db.version(3).stores(TempSchema);
|
|
db.version(4)
|
|
.stores(SettingsSchema)
|
|
.upgrade((tx) => {
|
|
return tx
|
|
.table("settings")
|
|
.toCollection()
|
|
.modify((settings) => {
|
|
settings.accountDid = ""; // make it non-null for the default master settings, but still indexable
|
|
});
|
|
});
|
|
|
|
const DEFAULT_SETTINGS: Settings = {
|
|
id: MASTER_SETTINGS_KEY,
|
|
activeDid: undefined,
|
|
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
|
};
|
|
|
|
// Event handler to initialize the non-sensitive database with default settings
|
|
db.on("populate", async () => {
|
|
await db.settings.add(DEFAULT_SETTINGS);
|
|
});
|
|
|
|
// Manage the encryption key.
|
|
|
|
// It's not really secure to maintain the secret next to the user's data.
|
|
// However, until we have better hooks into a real wallet or reliable secure
|
|
// storage, we'll do this for user convenience. As they sign more records
|
|
// and integrate with more people, they'll value it more and want to be more
|
|
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
|
|
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
|
|
// PWA so it's not in a browser... and then we hope to be integrated with a
|
|
// real wallet or something else more secure.
|
|
|
|
// One might ask: why encrypt at all? We figure a basic encryption is better
|
|
// than none. Plus, we expect to support their own password or keystore or
|
|
// external wallet as better signing options in the future, so it's gonna be
|
|
// important to have the structure where each account access might require
|
|
// user action.
|
|
|
|
// (Once upon a time we stored the secret in localStorage, but it frequently
|
|
// got erased, even though the IndexedDB still had the identity data. This
|
|
// ended up throwing lots of errors to the user... and they'd end up in a state
|
|
// where they couldn't take action because they couldn't unlock that identity.)
|
|
|
|
async function useSecretAndInitializeAccountsDB(
|
|
secretDB: SecretDexie,
|
|
accountsDB: SensitiveDexie,
|
|
): Promise<SensitiveDexie> {
|
|
return secretDB
|
|
.open()
|
|
.then(() => {
|
|
return secretDB.secret.get(MASTER_SECRET_KEY);
|
|
})
|
|
.then((secretRow?: Secret) => {
|
|
let secret = secretRow?.secret;
|
|
if (secret != null) {
|
|
// they already have it in IndexedDB, so just pass it along
|
|
return secret;
|
|
} else {
|
|
// check localStorage (for users before v 0.3.37)
|
|
const localSecret = localStorage.getItem("secret");
|
|
if (localSecret != null) {
|
|
// they had one, so we want to move it to IndexedDB
|
|
secret = localSecret;
|
|
} else {
|
|
// they didn't have one, so let's generate one
|
|
secret = Encryption.createRandomEncryptionKey();
|
|
}
|
|
// it is not in IndexedDB, so add it now
|
|
return secretDB.secret
|
|
.add({ id: MASTER_SECRET_KEY, secret })
|
|
.then(() => {
|
|
return secret;
|
|
});
|
|
}
|
|
})
|
|
.then((secret?: string) => {
|
|
if (secret == null) {
|
|
throw new Error("No secret found or created.");
|
|
} else {
|
|
// apply encryption to the sensitive database using the secret key
|
|
encrypted(accountsDB, { secretKey: secret });
|
|
accountsDB.version(1).stores(AccountsSchema);
|
|
accountsDB.open();
|
|
return accountsDB;
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
logConsoleAndDb("Error processing secret & encrypted accountsDB.", error);
|
|
// alert("There was an error processing encrypted data. See the Help page.");
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
// retrieves default settings
|
|
// calls db.open()
|
|
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
|
await db.open();
|
|
return (await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS;
|
|
}
|
|
|
|
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
|
if (!defaultSettings.activeDid) {
|
|
return defaultSettings;
|
|
} else {
|
|
const overrideSettings =
|
|
(await db.settings
|
|
.where("accountDid")
|
|
.equals(defaultSettings.activeDid)
|
|
.first()) || {};
|
|
return R.mergeDeepRight(defaultSettings, overrideSettings);
|
|
}
|
|
}
|
|
|
|
export async function updateDefaultSettings(
|
|
settingsChanges: Settings,
|
|
): Promise<void> {
|
|
delete settingsChanges.accountDid; // just in case
|
|
// ensure there is no "id" that would override the key
|
|
delete settingsChanges.id;
|
|
await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges);
|
|
}
|
|
|
|
export async function updateAccountSettings(
|
|
accountDid: string,
|
|
settingsChanges: Settings,
|
|
): Promise<void> {
|
|
settingsChanges.accountDid = accountDid;
|
|
delete settingsChanges.id; // key off account, not ID
|
|
const result = await db.settings
|
|
.where("accountDid")
|
|
.equals(settingsChanges.accountDid)
|
|
.modify(settingsChanges);
|
|
if (result === 0) {
|
|
if (!settingsChanges.id) {
|
|
// It is unfortunate that we have to set this explicitly.
|
|
// We didn't make id a "++id" at the beginning and Dexie won't let us change it,
|
|
// plus we made our first settings objects MASTER_SETTINGS_KEY = 1 instead of 0
|
|
settingsChanges.id = (await db.settings.count()) + 1;
|
|
}
|
|
await db.settings.add(settingsChanges);
|
|
}
|
|
}
|
|
|
|
export async function logToDb(message: string): Promise<void> {
|
|
await db.open();
|
|
const todayKey = new Date().toDateString();
|
|
// only keep one day's worth of logs
|
|
const previous = await db.logs.get(todayKey);
|
|
if (!previous) {
|
|
// when this is today's first log, clear out everything previous
|
|
// to avoid the log table getting too large
|
|
// (let's limit a different way someday)
|
|
await db.logs.clear();
|
|
}
|
|
const prevMessages = (previous && previous.message) || "";
|
|
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`;
|
|
await db.logs.update(todayKey, { message: fullMessage });
|
|
}
|
|
|
|
// similar method is in the sw_scripts/additional-scripts.js file
|
|
export async function logConsoleAndDb(
|
|
message: string,
|
|
isError = false,
|
|
): Promise<void> {
|
|
if (isError) {
|
|
logger.error(`${new Date().toISOString()} ${message}`);
|
|
} else {
|
|
logger.log(`${new Date().toISOString()} ${message}`);
|
|
}
|
|
await logToDb(message);
|
|
}
|
|
|