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.
420 lines
12 KiB
420 lines
12 KiB
import { IIdentifier } from "@veramo/core";
|
|
import { getRandomBytesSync } from "ethereum-cryptography/random";
|
|
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
|
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
|
import { HDNode } from "@ethersproject/hdnode";
|
|
|
|
import {
|
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
createEndorserJwtForDid,
|
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
|
CONTACT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
} from "../../libs/endorserServer";
|
|
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
|
import { logger } from "../../utils/logger";
|
|
|
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
|
|
|
export const LOCAL_KMS_NAME = "local";
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param {string} address
|
|
* @param {string} publicHex
|
|
* @param {string} privateHex
|
|
* @param {string} derivationPath
|
|
* @return {*} {Omit<IIdentifier, 'provider'>}
|
|
*/
|
|
export const newIdentifier = (
|
|
address: string,
|
|
publicHex: string,
|
|
privateHex: string,
|
|
derivationPath: string,
|
|
): Omit<IIdentifier, keyof "provider"> => {
|
|
return {
|
|
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
|
keys: [
|
|
{
|
|
kid: publicHex,
|
|
kms: LOCAL_KMS_NAME,
|
|
meta: { derivationPath: derivationPath },
|
|
privateKeyHex: privateHex,
|
|
publicKeyHex: publicHex,
|
|
type: "Secp256k1",
|
|
},
|
|
],
|
|
provider: DEFAULT_DID_PROVIDER_NAME,
|
|
services: [],
|
|
};
|
|
};
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param {string} mnemonic
|
|
* @return {[string, string, string, string]} address, privateHex, publicHex, derivationPath
|
|
*/
|
|
export const deriveAddress = (
|
|
mnemonic: string,
|
|
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
|
|
): [string, string, string, string] => {
|
|
mnemonic = mnemonic.trim().toLowerCase();
|
|
|
|
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
|
const rootNode: HDNode = hdnode.derivePath(derivationPath);
|
|
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
|
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
|
const address = rootNode.address;
|
|
|
|
return [address, privateHex, publicHex, derivationPath];
|
|
};
|
|
|
|
export const generateRandomBytes = (numBytes: number): Uint8Array => {
|
|
return getRandomBytesSync(numBytes);
|
|
};
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @return {*} {string}
|
|
*/
|
|
export const generateSeed = (): string => {
|
|
const entropy: Uint8Array = getRandomBytesSync(32);
|
|
const mnemonic = entropyToMnemonic(entropy, wordlist);
|
|
|
|
return mnemonic;
|
|
};
|
|
|
|
/**
|
|
* Retrieve an access token, or "" if no DID is provided.
|
|
*
|
|
* @param {string} did
|
|
* @return {string} JWT with basic payload
|
|
*/
|
|
export const accessToken = async (did?: string) => {
|
|
if (did) {
|
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
const endEpoch = nowEpoch + 60; // add one minute
|
|
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
|
return createEndorserJwtForDid(did, tokenPayload);
|
|
} else {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Extract JWT from various URL formats
|
|
* @param jwtUrlText The URL containing the JWT
|
|
* @returns The extracted JWT or null if not found
|
|
*/
|
|
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
|
|
try {
|
|
let jwtText = jwtUrlText;
|
|
|
|
// Try to extract JWT from URL paths
|
|
const paths = [
|
|
CONTACT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
|
];
|
|
|
|
for (const path of paths) {
|
|
const pathIndex = jwtText.indexOf(path);
|
|
if (pathIndex > -1) {
|
|
jwtText = jwtText.substring(pathIndex + path.length);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Validate JWT format
|
|
if (!jwtText.match(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/)) {
|
|
logger.error("Invalid JWT format in URL:", jwtUrlText);
|
|
return null;
|
|
}
|
|
|
|
return jwtText;
|
|
} catch (error) {
|
|
logger.error("Error extracting JWT from URL:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const nextDerivationPath = (origDerivPath: string) => {
|
|
let lastStr = origDerivPath.split("/").slice(-1)[0];
|
|
if (lastStr.endsWith("'")) {
|
|
lastStr = lastStr.slice(0, -1);
|
|
}
|
|
const lastNum = parseInt(lastStr, 10);
|
|
const newLastNum = lastNum + 1;
|
|
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
|
|
const newDerivPath = origDerivPath
|
|
.split("/")
|
|
.slice(0, -1)
|
|
.concat([newLastStr])
|
|
.join("/");
|
|
return newDerivPath;
|
|
};
|
|
|
|
// Base64 encoding/decoding utilities for browser
|
|
export function base64ToArrayBuffer(base64: string): Uint8Array {
|
|
const binaryString = atob(base64);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
|
return btoa(binary);
|
|
}
|
|
|
|
const SALT_LENGTH = 16;
|
|
const IV_LENGTH = 12;
|
|
const KEY_LENGTH = 256;
|
|
const ITERATIONS = 100000;
|
|
|
|
// Message encryption helper function, used for onboarding meeting messages
|
|
export async function encryptMessage(message: string, password: string) {
|
|
const encoder = new TextEncoder();
|
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
|
|
// Derive key from password using PBKDF2
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
"raw",
|
|
encoder.encode(password),
|
|
"PBKDF2",
|
|
false,
|
|
["deriveBits", "deriveKey"],
|
|
);
|
|
|
|
const key = await crypto.subtle.deriveKey(
|
|
{
|
|
name: "PBKDF2",
|
|
salt,
|
|
iterations: ITERATIONS,
|
|
hash: "SHA-256",
|
|
},
|
|
keyMaterial,
|
|
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
false,
|
|
["encrypt"],
|
|
);
|
|
|
|
// Encrypt the message
|
|
const encryptedContent = await crypto.subtle.encrypt(
|
|
{
|
|
name: "AES-GCM",
|
|
iv,
|
|
},
|
|
key,
|
|
encoder.encode(message),
|
|
);
|
|
|
|
// Return a JSON structure with base64-encoded components
|
|
const result = {
|
|
salt: arrayBufferToBase64(salt),
|
|
iv: arrayBufferToBase64(iv),
|
|
encrypted: arrayBufferToBase64(encryptedContent),
|
|
};
|
|
|
|
return btoa(JSON.stringify(result));
|
|
}
|
|
|
|
// Message decryption helper function, used for onboarding meeting messages
|
|
export async function decryptMessage(encryptedJson: string, password: string) {
|
|
const decoder = new TextDecoder();
|
|
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
|
|
|
// Convert base64 components back to Uint8Arrays
|
|
const saltArray = base64ToArrayBuffer(salt);
|
|
const ivArray = base64ToArrayBuffer(iv);
|
|
const encryptedContent = base64ToArrayBuffer(encrypted);
|
|
|
|
// Derive the same key using PBKDF2 with the extracted salt
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
"raw",
|
|
new TextEncoder().encode(password),
|
|
"PBKDF2",
|
|
false,
|
|
["deriveBits", "deriveKey"],
|
|
);
|
|
|
|
const key = await crypto.subtle.deriveKey(
|
|
{
|
|
name: "PBKDF2",
|
|
salt: saltArray,
|
|
iterations: ITERATIONS,
|
|
hash: "SHA-256",
|
|
},
|
|
keyMaterial,
|
|
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
false,
|
|
["decrypt"],
|
|
);
|
|
|
|
// Decrypt the content
|
|
const decryptedContent = await crypto.subtle.decrypt(
|
|
{
|
|
name: "AES-GCM",
|
|
iv: ivArray,
|
|
},
|
|
key,
|
|
encryptedContent,
|
|
);
|
|
|
|
// Convert the decrypted content back to a string
|
|
return decoder.decode(decryptedContent);
|
|
}
|
|
|
|
// Test function to verify encryption/decryption
|
|
export async function testMessageEncryptionDecryption() {
|
|
try {
|
|
const testMessage = "Hello, this is a test message! 🚀";
|
|
const testPassword = "myTestPassword123";
|
|
|
|
logger.log("Original message:", testMessage);
|
|
|
|
// Test encryption
|
|
logger.log("Encrypting...");
|
|
const encrypted = await encryptMessage(testMessage, testPassword);
|
|
logger.log("Encrypted result:", encrypted);
|
|
|
|
// Test decryption
|
|
logger.log("Decrypting...");
|
|
const decrypted = await decryptMessage(encrypted, testPassword);
|
|
logger.log("Decrypted result:", decrypted);
|
|
|
|
// Verify
|
|
const success = testMessage === decrypted;
|
|
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
|
logger.log("Messages match:", success);
|
|
|
|
// Test with wrong password
|
|
logger.log("\nTesting with wrong password...");
|
|
try {
|
|
await decryptMessage(encrypted, "wrongPassword");
|
|
logger.log("Incorrectly decrypted with wrong password ❌");
|
|
} catch (error) {
|
|
logger.log("Correctly failed to decrypt with wrong password ✅");
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
logger.error("Test failed with error:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
|
|
export async function simpleEncrypt(
|
|
text: string,
|
|
secret: ArrayBuffer,
|
|
): Promise<ArrayBuffer> {
|
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
|
|
// Derive a 256-bit key from the secret using SHA-256
|
|
const keyData = await crypto.subtle.digest(
|
|
"SHA-256",
|
|
secret,
|
|
);
|
|
const key = await crypto.subtle.importKey(
|
|
"raw",
|
|
keyData,
|
|
{ name: "AES-GCM" },
|
|
false,
|
|
["encrypt"],
|
|
);
|
|
|
|
const encrypted = await crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", iv },
|
|
key,
|
|
new TextEncoder().encode(text),
|
|
);
|
|
|
|
// Combine IV and encrypted data
|
|
const result = new Uint8Array(iv.length + encrypted.byteLength);
|
|
result.set(iv);
|
|
result.set(new Uint8Array(encrypted), iv.length);
|
|
|
|
return result.buffer;
|
|
}
|
|
|
|
// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
|
|
export async function simpleDecrypt(
|
|
encryptedText: ArrayBuffer,
|
|
secret: ArrayBuffer,
|
|
): Promise<string> {
|
|
const data = new Uint8Array(encryptedText);
|
|
|
|
// Extract IV and encrypted data
|
|
const iv = data.slice(0, 16);
|
|
const encrypted = data.slice(16);
|
|
|
|
// Derive the same 256-bit key from the secret using SHA-256
|
|
const keyData = await crypto.subtle.digest(
|
|
"SHA-256",
|
|
secret,
|
|
);
|
|
const key = await crypto.subtle.importKey(
|
|
"raw",
|
|
keyData,
|
|
{ name: "AES-GCM" },
|
|
false,
|
|
["decrypt"],
|
|
);
|
|
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: "AES-GCM", iv },
|
|
key,
|
|
encrypted,
|
|
);
|
|
|
|
return new TextDecoder().decode(decrypted);
|
|
}
|
|
|
|
// Test function for simple encryption/decryption
|
|
export async function testSimpleEncryptionDecryption() {
|
|
try {
|
|
const testMessage = "Hello, this is a test message! 🚀";
|
|
const testSecret = crypto.getRandomValues(new Uint8Array(32));
|
|
|
|
logger.log("Original message:", testMessage);
|
|
|
|
// Test encryption
|
|
logger.log("Encrypting...");
|
|
const encrypted = await simpleEncrypt(testMessage, testSecret);
|
|
const encryptedBase64 = arrayBufferToBase64(encrypted);
|
|
logger.log("Encrypted result:", encryptedBase64);
|
|
|
|
// Test decryption
|
|
logger.log("Decrypting...");
|
|
const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
|
|
const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
|
|
logger.log("Decrypted result:", decrypted);
|
|
|
|
// Verify
|
|
const success = testMessage === decrypted;
|
|
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
|
logger.log("Messages match:", success);
|
|
|
|
// Test with wrong secret
|
|
logger.log("\nTesting with wrong secret...");
|
|
try {
|
|
await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
|
|
logger.log("Incorrectly decrypted with wrong secret ❌");
|
|
} catch (error) {
|
|
logger.log("Correctly failed to decrypt with wrong secret ✅");
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
logger.error("Test failed with error:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|