forked from jsnbuchanan/crowd-funder-for-time-pwa
move low-level DID-related create & decode into separate folder (#120)
Co-authored-by: Trent Larson <trent@trentlarson.com> Reviewed-on: trent_larson/crowd-funder-for-time-pwa#120 Co-authored-by: trentlarson <trent@trentlarson.com> Co-committed-by: trentlarson <trent@trentlarson.com>
This commit is contained in:
@@ -47,7 +47,7 @@ npm run lint
|
|||||||
```
|
```
|
||||||
# (Let's replace this with a .env.development or .env.staging file.)
|
# (Let's replace this with a .env.development or .env.staging file.)
|
||||||
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
|
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app PASSKEYS_ENABLED=yep npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
* Production
|
* Production
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const DEFAULT_PUSH_SERVER =
|
|||||||
|
|
||||||
export const IMAGE_TYPE_PROFILE = "profile";
|
export const IMAGE_TYPE_PROFILE = "profile";
|
||||||
|
|
||||||
|
export const PASSKEYS_ENABLED =
|
||||||
|
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible values for "group" and "type" are in App.vue.
|
* The possible values for "group" and "type" are in App.vue.
|
||||||
* From the notiwind package
|
* From the notiwind package
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { getRandomBytesSync } from "ethereum-cryptography/random";
|
|||||||
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
||||||
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
||||||
import { HDNode } from "@ethersproject/hdnode";
|
import { HDNode } from "@ethersproject/hdnode";
|
||||||
import * as didJwt from "did-jwt";
|
|
||||||
import * as u8a from "uint8arrays";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createEndorserJwt,
|
createEndorserJwtForDid,
|
||||||
ENDORSER_JWT_URL_LOCATION,
|
ENDORSER_JWT_URL_LOCATION,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||||
|
import { decodeEndorserJwt } from "@/libs/crypto/vc";
|
||||||
|
|
||||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
||||||
|
|
||||||
@@ -95,58 +94,12 @@ export const accessToken = async (did?: string) => {
|
|||||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||||
const endEpoch = nowEpoch + 60; // add one minute
|
const endEpoch = nowEpoch + 60; // add one minute
|
||||||
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
||||||
return createEndorserJwt(did, tokenPayload);
|
return createEndorserJwtForDid(did, tokenPayload);
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Copied out of did-jwt since it's deprecated in that library.
|
|
||||||
*
|
|
||||||
* The SimpleSigner returns a configured function for signing data.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const signer = SimpleSigner(import.meta.env.PRIVATE_KEY)
|
|
||||||
* signer(data, (err, signature) => {
|
|
||||||
* ...
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* @param {String} hexPrivateKey a hex encoded private key
|
|
||||||
* @return {Function} a configured signer function
|
|
||||||
*/
|
|
||||||
export function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
|
|
||||||
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
|
|
||||||
return async (data) => {
|
|
||||||
const signature = (await signer(data)) as string;
|
|
||||||
return fromJose(signature);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// from did-jwt/util; see SimpleSigner above
|
|
||||||
export function fromJose(signature: string): {
|
|
||||||
r: string;
|
|
||||||
s: string;
|
|
||||||
recoveryParam?: number;
|
|
||||||
} {
|
|
||||||
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
|
||||||
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
|
||||||
throw new TypeError(
|
|
||||||
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const r = bytesToHex(signatureBytes.slice(0, 32));
|
|
||||||
const s = bytesToHex(signatureBytes.slice(32, 64));
|
|
||||||
const recoveryParam =
|
|
||||||
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
|
|
||||||
return { r, s, recoveryParam };
|
|
||||||
}
|
|
||||||
|
|
||||||
// from did-jwt/util; see SimpleSigner above
|
|
||||||
export function bytesToHex(b: Uint8Array): string {
|
|
||||||
return u8a.toString(b, "base16");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@return results of uportJwtPayload:
|
@return results of uportJwtPayload:
|
||||||
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
|
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
|
||||||
@@ -163,7 +116,7 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// JWT format: { header, payload, signature, data }
|
// JWT format: { header, payload, signature, data }
|
||||||
const jwt = didJwt.decodeJWT(jwtText);
|
const jwt = decodeEndorserJwt(jwtText);
|
||||||
|
|
||||||
return jwt.payload;
|
return jwt.payload;
|
||||||
};
|
};
|
||||||
|
|||||||
96
src/libs/crypto/vc/didPeer.ts
Normal file
96
src/libs/crypto/vc/didPeer.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {Buffer} from "buffer/";
|
||||||
|
import {decode as cborDecode} from "cbor-x";
|
||||||
|
import {bytesToMultibase, multibaseToBytes} from "did-jwt";
|
||||||
|
|
||||||
|
import {getWebCrypto} from "@/libs/crypto/vc/passkeyHelpers";
|
||||||
|
|
||||||
|
const PEER_DID_PREFIX = "did:peer:";
|
||||||
|
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function verifyPeerSignature(
|
||||||
|
payloadBytes: Buffer,
|
||||||
|
issuerDid: string,
|
||||||
|
signatureBytes: Uint8Array,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||||
|
|
||||||
|
const WebCrypto = await getWebCrypto();
|
||||||
|
const verifyAlgorithm = {
|
||||||
|
name: "ECDSA",
|
||||||
|
hash: { name: "SHA-256" },
|
||||||
|
};
|
||||||
|
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
|
||||||
|
const keyAlgorithm = {
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: publicKeyJwk.crv,
|
||||||
|
};
|
||||||
|
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
||||||
|
"jwk",
|
||||||
|
publicKeyJwk,
|
||||||
|
keyAlgorithm,
|
||||||
|
false,
|
||||||
|
["verify"],
|
||||||
|
);
|
||||||
|
const verified = await WebCrypto.subtle.verify(
|
||||||
|
verifyAlgorithm,
|
||||||
|
publicKeyCryptoKey,
|
||||||
|
signatureBytes,
|
||||||
|
payloadBytes,
|
||||||
|
);
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||||
|
const jwkObj = cborDecode(publicKeyBytes);
|
||||||
|
if (
|
||||||
|
jwkObj[1] != 2 || // kty "EC"
|
||||||
|
jwkObj[3] != -7 || // alg "ES256"
|
||||||
|
jwkObj[-1] != 1 || // crv "P-256"
|
||||||
|
jwkObj[-2].length != 32 || // x
|
||||||
|
jwkObj[-3].length != 32 // y
|
||||||
|
) {
|
||||||
|
throw new Error("Unable to extract key.");
|
||||||
|
}
|
||||||
|
const publicKeyJwk = {
|
||||||
|
alg: "ES256",
|
||||||
|
crv: "P-256",
|
||||||
|
kty: "EC",
|
||||||
|
x: arrayToBase64Url(jwkObj[-2]),
|
||||||
|
y: arrayToBase64Url(jwkObj[-3]),
|
||||||
|
};
|
||||||
|
const publicKeyBuffer = Buffer.concat([
|
||||||
|
Buffer.from(jwkObj[-2]),
|
||||||
|
Buffer.from(jwkObj[-3]),
|
||||||
|
]);
|
||||||
|
return { publicKeyJwk, publicKeyBuffer };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBase64Url(anythingB64: string) {
|
||||||
|
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayToBase64Url(anything: Uint8Array) {
|
||||||
|
return toBase64Url(Buffer.from(anything).toString("base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function peerDidToPublicKeyBytes(did: string) {
|
||||||
|
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPeerDid(publicKeyBytes: Uint8Array) {
|
||||||
|
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
|
||||||
|
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
|
||||||
|
const methodSpecificId = bytesToMultibase(
|
||||||
|
publicKeyBytes,
|
||||||
|
"base58btc",
|
||||||
|
"p256-pub",
|
||||||
|
);
|
||||||
|
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
|
||||||
|
}
|
||||||
110
src/libs/crypto/vc/index.ts
Normal file
110
src/libs/crypto/vc/index.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Verifiable Credential & DID functions, specifically for EndorserSearch.org tools
|
||||||
|
*
|
||||||
|
* The goal is to make this folder similar across projects, then move it to a library.
|
||||||
|
* Other projects: endorser-ch, image-api
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as didJwt from "did-jwt";
|
||||||
|
import { JWTDecoded } from "did-jwt/lib/JWT";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import * as u8a from "uint8arrays";
|
||||||
|
|
||||||
|
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta info about a key
|
||||||
|
*/
|
||||||
|
export interface KeyMeta {
|
||||||
|
/**
|
||||||
|
* Decentralized ID for the key
|
||||||
|
*/
|
||||||
|
did: string;
|
||||||
|
/**
|
||||||
|
* Stringified IIDentifier object from Veramo
|
||||||
|
*/
|
||||||
|
identity?: string;
|
||||||
|
/**
|
||||||
|
* The Webauthn credential ID in hex, if this is from a passkey
|
||||||
|
*/
|
||||||
|
passkeyCredIdHex?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
||||||
|
return !!keyMeta?.passkeyCredIdHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEndorserJwtForKey(
|
||||||
|
account: KeyMeta,
|
||||||
|
payload: object,
|
||||||
|
) {
|
||||||
|
if (account?.identity) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const identity: IIdentifier = JSON.parse(account.identity!);
|
||||||
|
const privateKeyHex = identity.keys[0].privateKeyHex;
|
||||||
|
const signer = await SimpleSigner(privateKeyHex as string);
|
||||||
|
return didJwt.createJWT(payload, {
|
||||||
|
issuer: account.did,
|
||||||
|
signer: signer,
|
||||||
|
});
|
||||||
|
} else if (account?.passkeyCredIdHex) {
|
||||||
|
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
|
||||||
|
} else {
|
||||||
|
throw new Error("No identity data found to sign for DID " + account.did);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied out of did-jwt since it's deprecated in that library.
|
||||||
|
*
|
||||||
|
* The SimpleSigner returns a configured function for signing data.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const signer = SimpleSigner(import.meta.env.PRIVATE_KEY)
|
||||||
|
* signer(data, (err, signature) => {
|
||||||
|
* ...
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* @param {String} hexPrivateKey a hex encoded private key
|
||||||
|
* @return {Function} a configured signer function
|
||||||
|
*/
|
||||||
|
function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
|
||||||
|
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
|
||||||
|
return async (data) => {
|
||||||
|
const signature = (await signer(data)) as string;
|
||||||
|
return fromJose(signature);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// from did-jwt/util; see SimpleSigner above
|
||||||
|
function fromJose(signature: string): {
|
||||||
|
r: string;
|
||||||
|
s: string;
|
||||||
|
recoveryParam?: number;
|
||||||
|
} {
|
||||||
|
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
||||||
|
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const r = bytesToHex(signatureBytes.slice(0, 32));
|
||||||
|
const s = bytesToHex(signatureBytes.slice(32, 64));
|
||||||
|
const recoveryParam =
|
||||||
|
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
|
||||||
|
return { r, s, recoveryParam };
|
||||||
|
}
|
||||||
|
|
||||||
|
// from did-jwt/util; see SimpleSigner above
|
||||||
|
function bytesToHex(b: Uint8Array): string {
|
||||||
|
return u8a.toString(b, "base16");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeEndorserJwt(jwt: string): JWTDecoded {
|
||||||
|
return didJwt.decodeJWT(jwt);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import { decode as cborDecode } from "cbor-x";
|
import { JWTPayload } from "did-jwt";
|
||||||
import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt";
|
|
||||||
import { DIDResolutionResult } from "did-resolver";
|
import { DIDResolutionResult } from "did-resolver";
|
||||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||||
import {
|
import {
|
||||||
@@ -21,10 +20,14 @@ import {
|
|||||||
} from "@simplewebauthn/types";
|
} from "@simplewebauthn/types";
|
||||||
|
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
|
import { unwrapEC2Signature } from "@/libs/crypto/vc/passkeyHelpers";
|
||||||
|
import {
|
||||||
|
arrayToBase64Url,
|
||||||
|
cborToKeys,
|
||||||
|
peerDidToPublicKeyBytes,
|
||||||
|
verifyPeerSignature,
|
||||||
|
} from "@/libs/crypto/vc/didPeer";
|
||||||
|
|
||||||
const PEER_DID_PREFIX = "did:peer:";
|
|
||||||
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
|
|
||||||
export interface JWK {
|
export interface JWK {
|
||||||
kty: string;
|
kty: string;
|
||||||
crv: string;
|
crv: string;
|
||||||
@@ -32,14 +35,6 @@ export interface JWK {
|
|||||||
y: string;
|
y: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBase64Url(anythingB64: string) {
|
|
||||||
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayToBase64Url(anything: Uint8Array) {
|
|
||||||
return toBase64Url(Buffer.from(anything).toString("base64"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function registerCredential(passkeyName?: string) {
|
export async function registerCredential(passkeyName?: string) {
|
||||||
const options: PublicKeyCredentialCreationOptionsJSON =
|
const options: PublicKeyCredentialCreationOptionsJSON =
|
||||||
await generateRegistrationOptions({
|
await generateRegistrationOptions({
|
||||||
@@ -92,21 +87,6 @@ export async function registerCredential(passkeyName?: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPeerDid(publicKeyBytes: Uint8Array) {
|
|
||||||
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
|
|
||||||
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
|
|
||||||
const methodSpecificId = bytesToMultibase(
|
|
||||||
publicKeyBytes,
|
|
||||||
"base58btc",
|
|
||||||
"p256-pub",
|
|
||||||
);
|
|
||||||
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerDidToPublicKeyBytes(did: string) {
|
|
||||||
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PeerSetup {
|
export class PeerSetup {
|
||||||
public authenticatorData?: ArrayBuffer;
|
public authenticatorData?: ArrayBuffer;
|
||||||
public challenge?: Uint8Array;
|
public challenge?: Uint8Array;
|
||||||
@@ -419,33 +399,7 @@ export async function verifyJwtWebCrypto(
|
|||||||
|
|
||||||
// Construct the preimage
|
// Construct the preimage
|
||||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||||
|
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
||||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
|
||||||
|
|
||||||
const WebCrypto = await getWebCrypto();
|
|
||||||
const verifyAlgorithm = {
|
|
||||||
name: "ECDSA",
|
|
||||||
hash: { name: "SHA-256" },
|
|
||||||
};
|
|
||||||
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
|
|
||||||
const keyAlgorithm = {
|
|
||||||
name: "ECDSA",
|
|
||||||
namedCurve: publicKeyJwk.crv,
|
|
||||||
};
|
|
||||||
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
|
||||||
"jwk",
|
|
||||||
publicKeyJwk,
|
|
||||||
keyAlgorithm,
|
|
||||||
false,
|
|
||||||
["verify"],
|
|
||||||
);
|
|
||||||
const verified = await WebCrypto.subtle.verify(
|
|
||||||
verifyAlgorithm,
|
|
||||||
publicKeyCryptoKey,
|
|
||||||
finalSigBuffer,
|
|
||||||
preimage,
|
|
||||||
);
|
|
||||||
return verified;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@@ -551,31 +505,6 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
|
|||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cborToKeys(publicKeyBytes: Uint8Array) {
|
|
||||||
const jwkObj = cborDecode(publicKeyBytes);
|
|
||||||
if (
|
|
||||||
jwkObj[1] != 2 || // kty "EC"
|
|
||||||
jwkObj[3] != -7 || // alg "ES256"
|
|
||||||
jwkObj[-1] != 1 || // crv "P-256"
|
|
||||||
jwkObj[-2].length != 32 || // x
|
|
||||||
jwkObj[-3].length != 32 // y
|
|
||||||
) {
|
|
||||||
throw new Error("Unable to extract key.");
|
|
||||||
}
|
|
||||||
const publicKeyJwk = {
|
|
||||||
alg: "ES256",
|
|
||||||
crv: "P-256",
|
|
||||||
kty: "EC",
|
|
||||||
x: arrayToBase64Url(jwkObj[-2]),
|
|
||||||
y: arrayToBase64Url(jwkObj[-3]),
|
|
||||||
};
|
|
||||||
const publicKeyBuffer = Buffer.concat([
|
|
||||||
Buffer.from(jwkObj[-2]),
|
|
||||||
Buffer.from(jwkObj[-3]),
|
|
||||||
]);
|
|
||||||
return { publicKeyJwk, publicKeyBuffer };
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async function pemToCryptoKey(pem: string) {
|
async function pemToCryptoKey(pem: string) {
|
||||||
const binaryDerString = atob(
|
const binaryDerString = atob(
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
|
||||||
import { LRUCache } from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
|
|
||||||
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
|
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { NonsensitiveDexie } from "@/db/index";
|
import { NonsensitiveDexie } from "@/db/index";
|
||||||
import { createDidPeerJwt } from "@/libs/didPeer";
|
|
||||||
import { getAccount, getIdentity } from "@/libs/util";
|
import { getAccount, getIdentity } from "@/libs/util";
|
||||||
|
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
|
||||||
|
|
||||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||||
// the object in RegisterAction claims
|
// the object in RegisterAction claims
|
||||||
@@ -692,7 +691,7 @@ export async function createAndSubmitClaim(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const vcJwt: string = await createEndorserJwt(issuerDid, vcPayload);
|
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
|
||||||
|
|
||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
@@ -722,6 +721,14 @@ export async function createAndSubmitClaim(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createEndorserJwtForDid(
|
||||||
|
issuerDid: string,
|
||||||
|
payload: object,
|
||||||
|
) {
|
||||||
|
const account = await getAccount(issuerDid);
|
||||||
|
return createEndorserJwtForKey(account as KeyMeta, payload);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An AcceptAction is when someone accepts some contract or pledge.
|
* An AcceptAction is when someone accepts some contract or pledge.
|
||||||
*
|
*
|
||||||
@@ -937,25 +944,7 @@ export async function createEndorserJwtVcFromClaim(
|
|||||||
credentialSubject: claim,
|
credentialSubject: claim,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return createEndorserJwt(issuerDid, vcPayload);
|
return createEndorserJwtForDid(issuerDid, vcPayload);
|
||||||
}
|
|
||||||
|
|
||||||
export async function createEndorserJwt(issuerDid: string, payload: object) {
|
|
||||||
const account = await getAccount(issuerDid);
|
|
||||||
if (account?.identity) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const identity = JSON.parse(account.identity!);
|
|
||||||
const privateKeyHex = identity.keys[0].privateKeyHex;
|
|
||||||
const signer = await SimpleSigner(privateKeyHex);
|
|
||||||
return didJwt.createJWT(payload, {
|
|
||||||
issuer: issuerDid,
|
|
||||||
signer: signer,
|
|
||||||
});
|
|
||||||
} else if (account?.passkeyCredIdHex) {
|
|
||||||
return createDidPeerJwt(issuerDid, account.passkeyCredIdHex, payload);
|
|
||||||
} else {
|
|
||||||
throw new Error("No identity data found to sign for DID " + issuerDid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function register(
|
export async function register(
|
||||||
@@ -980,7 +969,7 @@ export async function register(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
const vcJwt = await createEndorserJwt(activeDid, vcPayload);
|
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload);
|
||||||
|
|
||||||
const url = apiServer + "/api/v2/claim";
|
const url = apiServer + "/api/v2/claim";
|
||||||
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
||||||
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
|
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
|
||||||
import * as serverUtil from "@/libs/endorserServer";
|
import * as serverUtil from "@/libs/endorserServer";
|
||||||
import { createPeerDid, registerCredential } from "@/libs/didPeer";
|
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
|
||||||
|
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
import {KeyMeta} from "@/libs/crypto/vc";
|
||||||
|
import {createPeerDid} from "@/libs/crypto/vc/didPeer";
|
||||||
|
|
||||||
export const PRIVACY_MESSAGE =
|
export const PRIVACY_MESSAGE =
|
||||||
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
|
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
|
||||||
@@ -196,9 +198,11 @@ export function findAllVisibleToDids(
|
|||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
export interface AccountKeyInfo extends Account, KeyMeta {}
|
||||||
|
|
||||||
export const getAccount = async (
|
export const getAccount = async (
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
): Promise<Account | undefined> => {
|
): Promise<AccountKeyInfo | undefined> => {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const account = (await accountsDB.accounts
|
const account = (await accountsDB.accounts
|
||||||
.where("did")
|
.where("did")
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ import { useClipboard } from "@vueuse/core";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "@/constants/app";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
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 {
|
import {
|
||||||
@@ -93,8 +92,7 @@ import {
|
|||||||
nextDerivationPath,
|
nextDerivationPath,
|
||||||
} from "@/libs/crypto";
|
} from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
CONTACT_URL_PREFIX,
|
CONTACT_URL_PREFIX, createEndorserJwtForDid,
|
||||||
createEndorserJwt,
|
|
||||||
ENDORSER_JWT_URL_LOCATION,
|
ENDORSER_JWT_URL_LOCATION,
|
||||||
isDid,
|
isDid,
|
||||||
register,
|
register,
|
||||||
@@ -161,7 +159,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const vcJwt: string = await createEndorserJwt(identity.did, contactInfo);
|
const vcJwt: string = await createEndorserJwtForDid(identity.did, contactInfo);
|
||||||
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
||||||
this.qrValue = viewPrefix + vcJwt;
|
this.qrValue = viewPrefix + vcJwt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,16 +81,29 @@
|
|||||||
v-if="!activeDid"
|
v-if="!activeDid"
|
||||||
class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p class="text-lg mb-3">
|
<div v-if="PASSKEYS_ENABLED">
|
||||||
To recognize giving, have someone register you:
|
<p class="text-lg mb-3">
|
||||||
</p>
|
Choose how to see info from your contacts or share contributions:
|
||||||
<div class="flex justify-center">
|
</p>
|
||||||
<!-- <button-->
|
<div class="flex justify-between">
|
||||||
<!-- class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"-->
|
<button
|
||||||
<!-- @click="generateIdentifier()"-->
|
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||||
<!-- >-->
|
@click="generateIdentifier()"
|
||||||
<!-- Let me start the easiest (with a passkey).-->
|
>
|
||||||
<!-- </button>-->
|
Let me start the easiest (with a passkey).
|
||||||
|
</button>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'start' }"
|
||||||
|
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Give me all the options.
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="text-lg mb-3">
|
||||||
|
To recognize giving or collaborate, have someone register you:
|
||||||
|
</p>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||||
@@ -327,7 +340,7 @@ import FeedFilters from "@/components/FeedFilters.vue";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface, PASSKEYS_ENABLED } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
@@ -383,6 +396,7 @@ export default class HomeView extends Vue {
|
|||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
|
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
|
|||||||
@@ -246,14 +246,15 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import * as vcLib from "@/libs/crypto/vc";
|
||||||
import {
|
import {
|
||||||
PeerSetup,
|
PeerSetup,
|
||||||
verifyJwtP256,
|
verifyJwtP256,
|
||||||
verifyJwtSimplewebauthn,
|
verifyJwtSimplewebauthn,
|
||||||
verifyJwtWebCrypto,
|
verifyJwtWebCrypto,
|
||||||
} from "@/libs/didPeer";
|
} from "@/libs/crypto/vc/passkeyDidPeer";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import {AccountKeyInfo, getAccount, registerAndSavePasskey} from "@/libs/util";
|
||||||
import { registerAndSavePasskey } from "@/libs/util";
|
|
||||||
|
|
||||||
const inputFileNameRef = ref<Blob>();
|
const inputFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@@ -360,6 +361,13 @@ export default class Help extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createJwtSimplewebauthn() {
|
public async createJwtSimplewebauthn() {
|
||||||
|
const account: AccountKeyInfo | undefined = await getAccount(
|
||||||
|
this.activeDid || "",
|
||||||
|
);
|
||||||
|
if (!vcLib.isFromPasskey(account)) {
|
||||||
|
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.peerSetup = new PeerSetup();
|
this.peerSetup = new PeerSetup();
|
||||||
this.jwt = await this.peerSetup.createJwtSimplewebauthn(
|
this.jwt = await this.peerSetup.createJwtSimplewebauthn(
|
||||||
this.activeDid as string,
|
this.activeDid as string,
|
||||||
@@ -370,6 +378,13 @@ export default class Help extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createJwtNavigator() {
|
public async createJwtNavigator() {
|
||||||
|
const account: AccountKeyInfo | undefined = await getAccount(
|
||||||
|
this.activeDid || "",
|
||||||
|
);
|
||||||
|
if (!vcLib.isFromPasskey(account)) {
|
||||||
|
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.peerSetup = new PeerSetup();
|
this.peerSetup = new PeerSetup();
|
||||||
this.jwt = await this.peerSetup.createJwtNavigator(
|
this.jwt = await this.peerSetup.createJwtNavigator(
|
||||||
this.activeDid as string,
|
this.activeDid as string,
|
||||||
|
|||||||
Reference in New Issue
Block a user