Browse Source

fix import for derived accounts and hopefully make other account-access code more robust

ui-fixes-2025-06-w2
Trent Larson 6 days ago
parent
commit
dcd0cc4c20
  1. 2
      src/libs/crypto/index.ts
  2. 40
      src/libs/util.ts
  3. 15
      src/views/AccountViewView.vue
  4. 2
      src/views/ImportAccountView.vue
  5. 109
      src/views/ImportDerivedAccountView.vue
  6. 9
      src/views/QuickActionBvcEndView.vue

2
src/libs/crypto/index.ts

@ -32,7 +32,7 @@ export const newIdentifier = (
publicHex: string, publicHex: string,
privateHex: string, privateHex: string,
derivationPath: string, derivationPath: string,
): Omit<IIdentifier, keyof "provider"> => { ): IIdentifier => {
return { return {
did: DEFAULT_DID_PROVIDER_NAME + ":" + address, did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
keys: [ keys: [

40
src/libs/util.ts

@ -42,6 +42,8 @@ import { createPeerDid } from "../libs/crypto/vc/didPeer";
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer"; import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { sha256 } from "ethereum-cryptography/sha256";
import { IIdentifier } from "@veramo/core";
export interface GiverReceiverInputInfo { export interface GiverReceiverInputInfo {
did?: string; did?: string;
@ -622,14 +624,12 @@ export const retrieveFullyDecryptedAccount = async (
return result; return result;
}; };
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => { export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]> => {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`); const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[]; const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
let result = accounts.map((account) => { let result = accounts.map((account) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars return account as AccountEncrypted;
const { identity, mnemonic, ...metadata } = account;
return metadata as Account;
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage // one of the few times we use accountsDBPromise directly; try to avoid more usage
@ -638,7 +638,14 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
result = array.map((account) => { result = array.map((account) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity, mnemonic, ...metadata } = account; const { identity, mnemonic, ...metadata } = account;
return metadata as Account; // This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
const identityStr = JSON.stringify(identity);
const encryptedAccount = {
identityEncrBase64: sha256(new TextEncoder().encode(identityStr)).toString(),
mnemonicEncrBase64: sha256(new TextEncoder().encode(account.mnemonic)).toString(),
...metadata,
};
return encryptedAccount as AccountEncrypted;
}); });
} }
return result; return result;
@ -648,9 +655,8 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
* Saves a new identity to both SQL and Dexie databases * Saves a new identity to both SQL and Dexie databases
*/ */
export async function saveNewIdentity( export async function saveNewIdentity(
identity: string, identity: IIdentifier,
mnemonic: string, mnemonic: string,
newId: { did: string; keys: Array<{ publicKeyHex: string }> },
derivationPath: string, derivationPath: string,
): Promise<void> { ): Promise<void> {
try { try {
@ -666,7 +672,8 @@ export async function saveNewIdentity(
} }
const secretBase64 = secrets.values[0][0] as string; const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64); const secret = base64ToArrayBuffer(secretBase64);
const encryptedIdentity = await simpleEncrypt(identity, secret); const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
@ -675,13 +682,13 @@ export async function saveNewIdentity(
const params = [ const params = [
new Date().toISOString(), new Date().toISOString(),
derivationPath, derivationPath,
newId.did, identity.did,
encryptedIdentityBase64, encryptedIdentityBase64,
encryptedMnemonicBase64, encryptedMnemonicBase64,
newId.keys[0].publicKeyHex, identity.keys[0].publicKeyHex,
]; ];
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
await databaseUtil.updateDefaultSettings({ activeDid: newId.did }); await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage // one of the few times we use accountsDBPromise directly; try to avoid more usage
@ -689,12 +696,12 @@ export async function saveNewIdentity(
await accountsDB.accounts.add({ await accountsDB.accounts.add({
dateCreated: new Date().toISOString(), dateCreated: new Date().toISOString(),
derivationPath: derivationPath, derivationPath: derivationPath,
did: newId.did, did: identity.did,
identity: identity, identity: identityStr,
mnemonic: mnemonic, mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex, publicKeyHex: identity.keys[0].publicKeyHex,
}); });
await updateDefaultSettings({ activeDid: newId.did }); await updateDefaultSettings({ activeDid: identity.did });
} }
} catch (error) { } catch (error) {
logger.error("Failed to update default settings:", error); logger.error("Failed to update default settings:", error);
@ -715,9 +722,8 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
deriveAddress(mnemonic); deriveAddress(mnemonic);
const newId = newIdentifier(address, publicHex, privateHex, derivationPath); const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
await saveNewIdentity(identity, mnemonic, newId, derivationPath); await saveNewIdentity(newId, mnemonic, derivationPath);
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false }); await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await updateAccountSettings(newId.did, { isRegistered: false }); await updateAccountSettings(newId.did, { isRegistered: false });

15
src/views/AccountViewView.vue

@ -1356,18 +1356,7 @@ export default class AccountViewView extends Vue {
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
*/ */
async processIdentity() { async processIdentity() {
let account: Account | undefined = undefined; const account = await retrieveAccountMetadata(this.activeDid);
const platformService = PlatformServiceFactory.getInstance();
const dbAccount = await platformService.dbQuery(
"SELECT * FROM accounts WHERE did = ?",
[this.activeDid],
);
if (dbAccount) {
account = databaseUtil.mapQueryResultToValues(dbAccount)[0] as Account;
}
if (USE_DEXIE_DB) {
account = await retrieveAccountMetadata(this.activeDid);
}
if (account?.identity) { if (account?.identity) {
const identity = JSON.parse(account.identity as string) as IIdentifier; const identity = JSON.parse(account.identity as string) as IIdentifier;
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
@ -1375,8 +1364,10 @@ export default class AccountViewView extends Vue {
this.derivationPath = identity.keys[0].meta?.derivationPath as string; this.derivationPath = identity.keys[0].meta?.derivationPath as string;
await this.checkLimits(); await this.checkLimits();
} else if (account?.publicKeyHex) { } else if (account?.publicKeyHex) {
// use the backup values in the top level of the account object
this.publicHex = account.publicKeyHex as string; this.publicHex = account.publicKeyHex as string;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = account.derivationPath as string;
await this.checkLimits(); await this.checkLimits();
} }
} }

2
src/views/ImportAccountView.vue

@ -165,7 +165,7 @@ export default class ImportAccountView extends Vue {
await accountsDB.accounts.clear(); await accountsDB.accounts.clear();
} }
} }
saveNewIdentity(JSON.stringify(newId), mne, newId, this.derivationPath); await saveNewIdentity(newId, mne, this.derivationPath);
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {

109
src/views/ImportDerivedAccountView.vue

@ -20,18 +20,18 @@
Will increment the maximum known derivation path from the existing seed. Will increment the maximum known derivation path from the existing seed.
</p> </p>
<p v-if="didArrays.length > 1"> <p v-if="Object.keys(didArrays).length > 1">
Choose existing DIDs from same seed phrase to compute derivation. Choose existing DIDs from same seed phrase to compute derivation.
</p> </p>
<ul class="mb-4"> <ul class="mb-4">
<li <li
v-for="dids in didArrays" v-for="dids in Object.values(didArrays)"
:key="dids[0]" :key="dids[0].did"
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2" class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
@click="switchAccount(dids[0])" @click="switchAccount(dids[0].did)"
> >
<font-awesome <font-awesome
v-if="dids[0] == selectedArrayFirstDid" v-if="dids[0].did == selectedArrayFirstDid"
icon="circle" icon="circle"
class="fa-fw text-blue-500 text-xl mr-3" class="fa-fw text-blue-500 text-xl mr-3"
></font-awesome> ></font-awesome>
@ -41,8 +41,8 @@
class="fa-fw text-slate-400 text-xl mr-3" class="fa-fw text-slate-400 text-xl mr-3"
></font-awesome> ></font-awesome>
<span class="overflow-hidden"> <span class="overflow-hidden">
<div class="text-sm text-slate-500 truncate"> <div class="text-sm text-slate-500">
<code>{{ dids.join(",") }}</code> <code>{{ dids.map((d) => d.did).join(" ") }}</code>
</div> </div>
</span> </span>
</li> </li>
@ -69,6 +69,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalizedLoaded } from "vue-router"; import { Router, RouteLocationNormalizedLoaded } from "vue-router";
@ -80,12 +81,12 @@ import {
} from "../libs/crypto"; } from "../libs/crypto";
import { accountsDBPromise, db } from "../db/index"; import { accountsDBPromise, db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil"; import { retrieveAllAccountsMetadata, retrieveFullyDecryptedAccount, saveNewIdentity } from "../libs/util";
import { retrieveAllAccountsMetadata } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { Account } from "../db/tables/accounts"; import { Account, AccountEncrypted } from "../db/tables/accounts";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { USE_DEXIE_DB } from "@/constants/app"; import { USE_DEXIE_DB } from "@/constants/app";
@Component({ @Component({
components: {}, components: {},
}) })
@ -94,23 +95,22 @@ export default class ImportAccountView extends Vue {
$router!: Router; $router!: Router;
derivationPath = DEFAULT_ROOT_DERIVATION_PATH; derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
didArrays: Array<Array<string>> = []; didArrays: Record<string, Account[]> = {};
selectedArrayFirstDid = ""; selectedArrayFirstDid = "";
async mounted() { async mounted() {
const accounts: Account[] = await retrieveAllAccountsMetadata(); const accounts: AccountEncrypted[] = await retrieveAllAccountsMetadata();
const seedDids: Record<string, Array<string>> = {}; const decryptedAccounts: (Account | undefined)[] = await Promise.all(accounts.map(async (account) => {
accounts.forEach((account) => { return retrieveFullyDecryptedAccount(account.did);
// Since we're only getting metadata, we can't check mnemonic }));
// Instead, we'll group by derivation path const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter((account) => account !== undefined);
if (account.derivationPath) {
const prevDids: Array<string> = seedDids[account.derivationPath] || []; // group by account.mnemonic
seedDids[account.derivationPath] = prevDids.concat([account.did]); const groupedAccounts: Record<string, Account[]> = R.groupBy((a) => a.mnemonic || "", filteredDecryptedAccounts) as Record<string, Account[]>;
}
}); this.didArrays = groupedAccounts;
this.didArrays = Object.values(seedDids); if (Object.keys(this.didArrays).length > 0) {
if (this.didArrays.length > 0) { this.selectedArrayFirstDid = Object.values(this.didArrays)[0][0].did;
this.selectedArrayFirstDid = this.didArrays[0][0];
} }
} }
@ -124,60 +124,30 @@ export default class ImportAccountView extends Vue {
public async incrementDerivation() { public async incrementDerivation() {
// find the maximum derivation path for the selected DIDs // find the maximum derivation path for the selected DIDs
const selectedArray: Array<string> = const selectedArray: Array<Account> =
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) || Object.values(this.didArrays).find((dids) => dids[0].did === this.selectedArrayFirstDid) ||
[]; [];
const platformService = PlatformServiceFactory.getInstance(); // extract the derivationPath array and sort it
const qmarks = selectedArray.map(() => "?").join(","); const derivationPaths = selectedArray.map((account) => account.derivationPath);
const queryResult = await platformService.dbQuery( derivationPaths.sort((a, b) => {
`SELECT * FROM accounts WHERE did IN (${qmarks})`, const aParts = a?.split("/");
selectedArray, const aLast = aParts?.[aParts.length - 1];
); const bParts = b?.split("/");
let allMatchingAccounts = databaseUtil.mapQueryResultToValues( const bLast = bParts?.[bParts.length - 1];
queryResult, return parseInt(aLast || "0") - parseInt(bLast || "0");
) as unknown as Account[];
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise; // let's match derived accounts differently so we don't need the private info
allMatchingAccounts = (await accountsDB.accounts
.where("did")
.anyOf(...selectedArray)
.toArray()) as Account[];
}
const accountWithMaxDeriv = allMatchingAccounts[0];
allMatchingAccounts.slice(1).forEach((account) => {
if (
account.derivationPath &&
accountWithMaxDeriv.derivationPath &&
account.derivationPath > accountWithMaxDeriv.derivationPath
) {
accountWithMaxDeriv.derivationPath = account.derivationPath;
}
}); });
// increment the last number in that max derivation path // we're sure there's at least one
const newDerivPath = nextDerivationPath( const maxDerivPath: string = derivationPaths[derivationPaths.length - 1] as string;
accountWithMaxDeriv.derivationPath as string,
);
const mne: string = accountWithMaxDeriv.mnemonic as string; const newDerivPath = nextDerivationPath(maxDerivPath);
const mne = selectedArray[0].mnemonic as string;
const [address, privateHex, publicHex] = deriveAddress(mne, newDerivPath); const [address, privateHex, publicHex] = deriveAddress(mne, newDerivPath);
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath); const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
try { try {
const { sql, params } = databaseUtil.generateInsertStatement( await saveNewIdentity(newId, mne, newDerivPath);
{
dateCreated: new Date().toISOString(),
derivationPath: newDerivPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
},
"accounts",
);
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise; const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add({ await accountsDB.accounts.add({
@ -191,6 +161,7 @@ export default class ImportAccountView extends Vue {
} }
// record that as the active DID // record that as the active DID
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("UPDATE settings SET activeDid = ?", [ await platformService.dbExec("UPDATE settings SET activeDid = ?", [
newId.did, newId.did,
]); ]);

9
src/views/QuickActionBvcEndView.vue

@ -167,6 +167,7 @@ import {
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { retrieveAllAccountsMetadata } from "@/libs/util";
@Component({ @Component({
methods: { claimSpecialDescription }, methods: { claimSpecialDescription },
components: { components: {
@ -229,13 +230,7 @@ export default class QuickActionBvcBeginView extends Vue {
suppressMilliseconds: true, suppressMilliseconds: true,
}) || ""; }) || "";
const queryResult = await platformService.dbQuery( this.allMyDids = (await retrieveAllAccountsMetadata()).map((account) => account.did);
"SELECT did FROM accounts",
);
this.allMyDids =
databaseUtil
.mapQueryResultToValues(queryResult)
?.map((row) => row[0] as string) || [];
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise; const accountsDB = await accountsDBPromise;
await accountsDB.open(); await accountsDB.open();

Loading…
Cancel
Save