forked from trent_larson/crowd-funder-for-time-pwa
add link directly into contact page to add a new contact via "contactJwt" query parameter
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -37,6 +37,7 @@
|
|||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
"dexie-export-import": "^4.1.1",
|
"dexie-export-import": "^4.1.1",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
|
"did-resolver": "^4.1.0",
|
||||||
"ethereum-cryptography": "^2.1.3",
|
"ethereum-cryptography": "^2.1.3",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
@@ -10954,7 +10955,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/base64url": {
|
"node_modules/base64url": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -12422,7 +12424,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/did-resolver": {
|
"node_modules/did-resolver": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"license": "Apache-2.0"
|
"resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA=="
|
||||||
},
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
"dexie-export-import": "^4.1.1",
|
"dexie-export-import": "^4.1.1",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
|
"did-resolver": "^4.1.0",
|
||||||
"ethereum-cryptography": "^2.1.3",
|
"ethereum-cryptography": "^2.1.3",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
|
|||||||
17
src/App.vue
17
src/App.vue
@@ -379,6 +379,7 @@ import { Vue, Component } from "vue-facing-decorator";
|
|||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
|
||||||
import { retrieveSettingsForActiveAccount } from "@/db/index";
|
import { retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
|
import { urlBase64ToUint8Array } from "@/libs/crypto/vc/util";
|
||||||
|
|
||||||
interface ServiceWorkerMessage {
|
interface ServiceWorkerMessage {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -666,20 +667,6 @@ export default class App extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
||||||
const base64 = (base64String + padding)
|
|
||||||
.replace(/-/g, "+")
|
|
||||||
.replace(/_/g, "/");
|
|
||||||
const rawData = window.atob(base64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
private subscribeToPush(): Promise<void> {
|
private subscribeToPush(): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
||||||
@@ -694,7 +681,7 @@ export default class App extends Vue {
|
|||||||
return reject(new Error(errorMsg));
|
return reject(new Error(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
const applicationServerKey = urlBase64ToUint8Array(this.b64);
|
||||||
const options: PushSubscriptionOptions = {
|
const options: PushSubscriptionOptions = {
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: applicationServerKey,
|
applicationServerKey: applicationServerKey,
|
||||||
|
|||||||
46
src/libs/crypto/vc/did-eth-local-resolver.ts
Normal file
46
src/libs/crypto/vc/did-eth-local-resolver.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* This did:ethr resolver instructs the did-jwt machinery to use the
|
||||||
|
* EcdsaSecp256k1RecoveryMethod2020Uses verification method which adds the recovery bit to the
|
||||||
|
* signature to recover the DID's public key from a signature.
|
||||||
|
*
|
||||||
|
* This effectively hard codes the did:ethr DID resolver to use the address as the public key.
|
||||||
|
* @param did : string
|
||||||
|
* @returns {Promise<DIDResolutionResult>}
|
||||||
|
*
|
||||||
|
* Similar code resides in image-api
|
||||||
|
*/
|
||||||
|
export const didEthLocalResolver = async (did: string) => {
|
||||||
|
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/;
|
||||||
|
const match = did.match(didRegex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const address = match[1]; // Extract eth address: 0x...
|
||||||
|
const publicKeyHex = address; // Use the address directly as a public key placeholder
|
||||||
|
|
||||||
|
return {
|
||||||
|
didDocumentMetadata: {},
|
||||||
|
didResolutionMetadata: {
|
||||||
|
contentType: "application/did+ld+json",
|
||||||
|
},
|
||||||
|
didDocument: {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
|
||||||
|
],
|
||||||
|
id: did,
|
||||||
|
verificationMethod: [
|
||||||
|
{
|
||||||
|
id: `${did}#controller`,
|
||||||
|
type: "EcdsaSec256k1RecoveryMethod2020",
|
||||||
|
controller: did,
|
||||||
|
blockchainAccountId: "eip155:1:" + publicKeyHex,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authentication: [`${did}#controller`],
|
||||||
|
assertionMethod: [`${did}#controller`],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported DID format: ${did}`);
|
||||||
|
};
|
||||||
@@ -6,14 +6,22 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Buffer } from "buffer/";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
|
import { JWTVerified } from "did-jwt";
|
||||||
import { JWTDecoded } from "did-jwt/lib/JWT";
|
import { JWTDecoded } from "did-jwt/lib/JWT";
|
||||||
|
import { Resolver } from "did-resolver";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import * as u8a from "uint8arrays";
|
import * as u8a from "uint8arrays";
|
||||||
|
|
||||||
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
|
import { didEthLocalResolver } from "./did-eth-local-resolver";
|
||||||
|
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
||||||
|
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
||||||
|
import { urlBase64ToUint8Array } from "./util";
|
||||||
|
|
||||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
export const ETHR_DID_PREFIX = "did:ethr:";
|
||||||
|
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
||||||
|
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meta info about a key
|
* Meta info about a key
|
||||||
@@ -33,6 +41,8 @@ export interface KeyMeta {
|
|||||||
passkeyCredIdHex?: string;
|
passkeyCredIdHex?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolver = new Resolver({ ethr: didEthLocalResolver });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tell whether a key is from a passkey
|
* Tell whether a key is from a passkey
|
||||||
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
|
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
|
||||||
@@ -107,6 +117,78 @@ function bytesToHex(b: Uint8Array): string {
|
|||||||
return u8a.toString(b, "base16");
|
return u8a.toString(b, "base16");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We should be calling 'verify' in more places, showing warnings if it fails.
|
||||||
export function decodeEndorserJwt(jwt: string): JWTDecoded {
|
export function decodeEndorserJwt(jwt: string): JWTDecoded {
|
||||||
return didJwt.decodeJWT(jwt);
|
return didJwt.decodeJWT(jwt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return Promise of at least { issuer, payload, verified boolean }
|
||||||
|
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
|
||||||
|
export async function decodeAndVerifyJwt(
|
||||||
|
jwt: string,
|
||||||
|
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
|
||||||
|
const pieces = jwt.split(".");
|
||||||
|
console.log("WTF decodeAndVerifyJwt", typeof jwt, jwt, pieces);
|
||||||
|
const header = JSON.parse(base64urlDecodeString(pieces[0]));
|
||||||
|
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
|
||||||
|
console.log("WTF decodeAndVerifyJwt after", header, payload);
|
||||||
|
const issuerDid = payload.iss;
|
||||||
|
if (!issuerDid) {
|
||||||
|
return Promise.reject({
|
||||||
|
clientError: {
|
||||||
|
message: `Missing "iss" field in JWT.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
||||||
|
try {
|
||||||
|
const verified = await didJwt.verifyJWT(jwt, { resolver });
|
||||||
|
return verified;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return Promise.reject({
|
||||||
|
clientError: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
message: `JWT failed verification: ` + e.toString(),
|
||||||
|
code: JWT_VERIFY_FAILED_CODE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") {
|
||||||
|
const verified = await verifyPeerSignature(
|
||||||
|
Buffer.from(payload),
|
||||||
|
issuerDid,
|
||||||
|
urlBase64ToUint8Array(pieces[2]),
|
||||||
|
);
|
||||||
|
if (!verified) {
|
||||||
|
return Promise.reject({
|
||||||
|
clientError: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
message: `JWT failed verification: ` + e.toString(),
|
||||||
|
code: JWT_VERIFY_FAILED_CODE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return { issuer: issuerDid, payload: payload, verified: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
|
||||||
|
return Promise.reject({
|
||||||
|
clientError: {
|
||||||
|
message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject({
|
||||||
|
clientError: {
|
||||||
|
message: `Unsupported DID method ${issuerDid}`,
|
||||||
|
code: UNSUPPORTED_DID_METHOD_CODE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -470,8 +470,18 @@ ${pubKeyBuffer.toString("base64")}
|
|||||||
return pem;
|
return pem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tried the base64url library but got an error using their Buffer
|
||||||
|
export function base64urlDecodeString(input: string) {
|
||||||
|
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// tried the base64url library but got an error using their Buffer
|
||||||
|
export function base64urlEncodeString(input: string) {
|
||||||
|
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function base64urlDecode(input: string) {
|
function base64urlDecodeArrayBuffer(input: string) {
|
||||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
||||||
const str = atob(input + pad);
|
const str = atob(input + pad);
|
||||||
@@ -483,9 +493,9 @@ function base64urlDecode(input: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function base64urlEncode(buffer: ArrayBuffer) {
|
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
||||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||||
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
return base64urlEncodeString(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
// from @simplewebauthn/browser
|
// from @simplewebauthn/browser
|
||||||
|
|||||||
11
src/libs/crypto/vc/util.ts
Normal file
11
src/libs/crypto/vc/util.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
@@ -270,6 +270,14 @@ export interface ErrorResult extends ResultWithType {
|
|||||||
|
|
||||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
name: string;
|
||||||
|
publicEncKey: string;
|
||||||
|
registered: boolean;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
nextPublicEncKeyHash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// This is used to check for hidden info.
|
// This is used to check for hidden info.
|
||||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||||
const HIDDEN_DID = "did:none:HIDDEN";
|
const HIDDEN_DID = "did:none:HIDDEN";
|
||||||
@@ -934,17 +942,12 @@ export async function generateEndorserJwtForAccount(
|
|||||||
isRegistered?: boolean,
|
isRegistered?: boolean,
|
||||||
name?: string,
|
name?: string,
|
||||||
profileImageUrl?: string,
|
profileImageUrl?: string,
|
||||||
|
// note that including the next key pushes QR codes to the next resolution smaller
|
||||||
|
includeNextKeyIfDerived?: boolean,
|
||||||
) {
|
) {
|
||||||
const publicKeyHex = account.publicKeyHex;
|
const publicKeyHex = account.publicKeyHex;
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
|
|
||||||
interface UserInfo {
|
|
||||||
name: string;
|
|
||||||
publicEncKey: string;
|
|
||||||
registered: boolean;
|
|
||||||
profileImageUrl?: string;
|
|
||||||
nextPublicEncKeyHash?: string;
|
|
||||||
}
|
|
||||||
const contactInfo = {
|
const contactInfo = {
|
||||||
iat: Date.now(),
|
iat: Date.now(),
|
||||||
iss: account.did,
|
iss: account.did,
|
||||||
@@ -958,7 +961,7 @@ export async function generateEndorserJwtForAccount(
|
|||||||
contactInfo.own.profileImageUrl = profileImageUrl;
|
contactInfo.own.profileImageUrl = profileImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account?.mnemonic && account?.derivationPath) {
|
if (includeNextKeyIfDerived && account?.mnemonic && account?.derivationPath) {
|
||||||
const newDerivPath = nextDerivationPath(account.derivationPath as string);
|
const newDerivPath = nextDerivationPath(account.derivationPath as string);
|
||||||
const nextPublicHex = deriveAddress(
|
const nextPublicHex = deriveAddress(
|
||||||
account.mnemonic as string,
|
account.mnemonic as string,
|
||||||
|
|||||||
@@ -130,9 +130,7 @@ export default class ContactImportView extends Vue {
|
|||||||
const importedContacts =
|
const importedContacts =
|
||||||
((this.$route as Router).query["contacts"] as string) || "[]";
|
((this.$route as Router).query["contacts"] as string) || "[]";
|
||||||
this.contactsImporting = JSON.parse(importedContacts);
|
this.contactsImporting = JSON.parse(importedContacts);
|
||||||
this.contactsSelected = new Array(this.contactsImporting.length).fill(
|
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
const baseContacts = await db.contacts.toArray();
|
const baseContacts = await db.contacts.toArray();
|
||||||
@@ -158,9 +156,9 @@ export default class ContactImportView extends Vue {
|
|||||||
if (R.isEmpty(differences)) {
|
if (R.isEmpty(differences)) {
|
||||||
this.sameCount++;
|
this.sameCount++;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// automatically import new data
|
// don't automatically import previous data
|
||||||
this.contactsSelected[i] = true;
|
this.contactsSelected[i] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,8 +90,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { Buffer } from "buffer/";
|
|
||||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
@@ -104,15 +102,8 @@ import { NotificationIface } from "@/constants/app";
|
|||||||
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
|
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
deriveAddress,
|
|
||||||
getContactPayloadFromJwtUrl,
|
|
||||||
nextDerivationPath,
|
|
||||||
} from "@/libs/crypto";
|
|
||||||
import {
|
|
||||||
CONTACT_URL_PREFIX,
|
|
||||||
createEndorserJwtForDid,
|
|
||||||
ENDORSER_JWT_URL_LOCATION,
|
|
||||||
generateEndorserJwtForAccount,
|
generateEndorserJwtForAccount,
|
||||||
isDid,
|
isDid,
|
||||||
register,
|
register,
|
||||||
@@ -153,37 +144,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
||||||
if (account) {
|
if (account) {
|
||||||
const publicKeyHex = account.publicKeyHex;
|
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
|
||||||
|
|
||||||
const contactInfo = {
|
|
||||||
iat: Date.now(),
|
|
||||||
iss: this.activeDid,
|
|
||||||
own: {
|
|
||||||
name:
|
|
||||||
(settings.firstName || "") +
|
|
||||||
(settings.lastName ? ` ${settings.lastName}` : ""), // lastName is deprecated, pre v 0.1.3
|
|
||||||
publicEncKey,
|
|
||||||
profileImageUrl: settings.profileImageUrl,
|
|
||||||
registered: settings.isRegistered,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (account?.mnemonic && account?.derivationPath) {
|
|
||||||
const newDerivPath = nextDerivationPath(account.derivationPath);
|
|
||||||
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
|
||||||
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
|
||||||
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
|
||||||
const nextPublicEncKeyHashBase64 =
|
|
||||||
Buffer.from(nextPublicEncKeyHash).toString("base64");
|
|
||||||
contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vcJwt = await createEndorserJwtForDid(this.activeDid, contactInfo);
|
|
||||||
|
|
||||||
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
|
||||||
viewPrefix + vcJwt;
|
|
||||||
|
|
||||||
const name =
|
const name =
|
||||||
(settings.firstName || "") +
|
(settings.firstName || "") +
|
||||||
(settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
|
(settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
|
||||||
@@ -193,6 +153,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
!!settings.isRegistered,
|
!!settings.isRegistered,
|
||||||
name,
|
name,
|
||||||
settings.profileImageUrl,
|
settings.profileImageUrl,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ import {
|
|||||||
} from "@/db/index";
|
} from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
|
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
|
||||||
|
import { decodeEndorserJwt } from "@/libs/crypto/vc";
|
||||||
import {
|
import {
|
||||||
CONTACT_CSV_HEADER,
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_URL_PREFIX,
|
CONTACT_URL_PREFIX,
|
||||||
@@ -307,6 +308,7 @@ import {
|
|||||||
isDid,
|
isDid,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
|
UserInfo,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
@@ -374,6 +376,24 @@ export default class ContactsView extends Vue {
|
|||||||
this.contacts = baseContacts.sort((a, b) =>
|
this.contacts = baseContacts.sort((a, b) =>
|
||||||
(a.name || "").localeCompare(b.name || ""),
|
(a.name || "").localeCompare(b.name || ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const importedContactJwt = (this.$route as Router).query[
|
||||||
|
"contactJwt"
|
||||||
|
] as string;
|
||||||
|
if (importedContactJwt) {
|
||||||
|
// really should fully verify
|
||||||
|
const { payload } = decodeEndorserJwt(importedContactJwt);
|
||||||
|
const userInfo = payload["own"] as UserInfo;
|
||||||
|
const newContact = {
|
||||||
|
did: payload["iss"],
|
||||||
|
name: userInfo.name,
|
||||||
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
||||||
|
profileImageUrl: userInfo.profileImageUrl,
|
||||||
|
publicKeyBase64: userInfo.publicEncKey,
|
||||||
|
registered: userInfo.registered,
|
||||||
|
} as Contact;
|
||||||
|
this.addContact(newContact);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private danger(message: string, title: string = "Error", timeout = 5000) {
|
private danger(message: string, title: string = "Error", timeout = 5000) {
|
||||||
@@ -891,7 +911,10 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.error("Got strange result from setting visibility:", result);
|
console.error(
|
||||||
|
"Got strange result from setting visibility. It can happen when setting visibility on oneself.",
|
||||||
|
result,
|
||||||
|
);
|
||||||
const message =
|
const message =
|
||||||
(result.error as string) || "Could not set visibility on the server.";
|
(result.error as string) || "Could not set visibility on the server.";
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showGeneralAdvanced && includeLocation && false"
|
v-if="showGeneralAdvanced && includeLocation"
|
||||||
class="items-center mb-4"
|
class="items-center mb-4"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
isRegistered,
|
isRegistered,
|
||||||
givenName,
|
givenName,
|
||||||
profileImageUrl,
|
profileImageUrl,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(message)
|
.copy(message)
|
||||||
|
|||||||
Reference in New Issue
Block a user