forked from jsnbuchanan/crowd-funder-for-time-pwa
(chore): merge mostly pathway changes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user