(chore): merge mostly pathway changes

This commit is contained in:
Matthew Raymer
2025-02-06 13:34:48 +00:00
20 changed files with 2399 additions and 112 deletions

View File

@@ -52,7 +52,7 @@ export const newIdentifier = (
*
*
* @param {string} mnemonic
* @return {*} {[string, string, string, string]}
* @return {[string, string, string, string]} address, privateHex, publicHex, derivationPath
*/
export const deriveAddress = (
mnemonic: string,
@@ -88,7 +88,8 @@ export const generateSeed = (): string => {
/**
* Retrieve an access token, or "" if no DID is provided.
*
* @return {*}
* @param {string} did
* @return {string} JWT with basic payload
*/
export const accessToken = async (did?: string) => {
if (did) {
@@ -147,3 +148,156 @@ export const nextDerivationPath = (origDerivPath: string) => {
.join("/");
return newDerivPath;
};
// Base64 encoding/decoding utilities for browser
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;
}
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;
// Encryption helper function
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));
}
// Decryption helper function
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 testEncryptionDecryption() {
try {
const testMessage = "Hello, this is a test message! 🚀";
const testPassword = "myTestPassword123";
console.log("Original message:", testMessage);
// Test encryption
console.log("Encrypting...");
const encrypted = await encryptMessage(testMessage, testPassword);
console.log("Encrypted result:", encrypted);
// Test decryption
console.log("Decrypting...");
const decrypted = await decryptMessage(encrypted, testPassword);
console.log("Decrypted result:", decrypted);
// Verify
const success = testMessage === decrypted;
console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
console.log("Messages match:", success);
// Test with wrong password
console.log("\nTesting with wrong password...");
try {
await decryptMessage(encrypted, "wrongPassword");
console.log("Should not reach here");
} catch (error) {
console.log("Correctly failed with wrong password ✅");
}
return success;
} catch (error) {
console.error("Test failed with error:", error);
return false;
}
}

View File

@@ -621,41 +621,6 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
/**
* Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify
*
* @param error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function errorStringForLog(error: any) {
let stringifiedError = "" + error;
try {
stringifiedError = JSON.stringify(error);
} catch (e) {
// can happen with Dexie, eg:
// TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'DexieError2'
// | property '_promise' -> object with constructor 'DexiePromise'
// --- property '_value' closes the circle
}
let fullError = "" + error + " - JSON: " + stringifiedError;
const errorResponseText = JSON.stringify(error.response);
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
// add error.response stuff
if (R.equals(error?.config, error?.response?.config)) {
// but exclude "config" because it's already in there
const newErrorResponseText = JSON.stringify(
R.omit(["config"] as never[], error.response),
);
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
} else {
fullError += " - .response JSON: " + errorResponseText;
}
}
return fullError;
}
/**
* @param handleId nullable, in which case "undefined" will be returned
* @param requesterDid optional, in which case no private info will be returned
@@ -710,6 +675,56 @@ export async function setPlanInCache(
planCache.set(handleId, planSummary);
}
/**
*
* @param error that is thrown from an Endorser server call by Axios
* @returns user-friendly message, or undefined if none found
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serverMessageForUser(error: any) {
return (
// this is how most user messages are returned
error?.response?.data?.error?.message
// some are returned as "error" with a string, but those are more for devs and are less helpful to the user
);
}
/**
* Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify
* It works with AxiosError, eg handling an error.response intelligently.
*
* @param error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function errorStringForLog(error: any) {
let stringifiedError = "" + error;
try {
stringifiedError = JSON.stringify(error);
} catch (e) {
// can happen with Dexie, eg:
// TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'DexieError2'
// | property '_promise' -> object with constructor 'DexiePromise'
// --- property '_value' closes the circle
}
let fullError = "" + error + " - JSON: " + stringifiedError;
const errorResponseText = JSON.stringify(error.response);
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
// add error.response stuff
if (R.equals(error?.config, error?.response?.config)) {
// but exclude "config" because it's already in there
const newErrorResponseText = JSON.stringify(
R.omit(["config"] as never[], error.response),
);
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
} else {
fullError += " - .response JSON: " + errorResponseText;
}
}
return fullError;
}
/**
*
* @returns { data: Array<OfferSummaryRecord>, hitLimit: boolean true if maximum was hit and there may be more }
@@ -1113,7 +1128,7 @@ export async function createAndSubmitClaim(
} catch (error: any) {
console.error("Error submitting claim:", error);
const errorMessage: string =
error.response?.data?.error?.message ||
serverMessageForUser(error) ||
error.message ||
"Got some error submitting the claim. Check your permissions, network, and error logs.";

View File

@@ -112,6 +112,21 @@ export const isGiveAction = (
return isGiveClaimType(veriClaim.claimType);
};
export const shortDid = (did: string) => {
if (did.startsWith("did:peer:")) {
return (
did.substring(0, "did:peer:".length + 2) +
"..." +
did.substring("did:peer:".length + 18, "did:peer:".length + 25) +
"..."
);
} else if (did.startsWith("did:ethr:")) {
return did.substring(0, "did:ethr:".length + 9) + "...";
} else {
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
}
}
export const nameForDid = (
activeDid: string,
contacts: Array<Contact>,