|
@ -1,37 +1,36 @@ |
|
|
|
|
|
|
|
|
function bufferFromBase64(base64) { |
|
|
function bufferFromBase64(base64) { |
|
|
const binaryString = atob(base64); |
|
|
const binaryString = atob(base64); |
|
|
const length = binaryString.length; |
|
|
const length = binaryString.length; |
|
|
const bytes = new Uint8Array(length); |
|
|
const bytes = new Uint8Array(length); |
|
|
|
|
|
|
|
|
for (let i = 0; i < length; i++) { |
|
|
for (let i = 0; i < length; i++) { |
|
|
bytes[i] = binaryString.charCodeAt(i); |
|
|
bytes[i] = binaryString.charCodeAt(i); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return bytes; |
|
|
return bytes; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function fromString(str, encoding = "utf8") { |
|
|
function fromString(str, encoding = "utf8") { |
|
|
if (encoding === "utf8") { |
|
|
if (encoding === "utf8") { |
|
|
return new TextEncoder().encode(str); |
|
|
return new TextEncoder().encode(str); |
|
|
} else if (encoding === "base16") { |
|
|
} else if (encoding === "base16") { |
|
|
if (str.length % 2 !== 0) { |
|
|
if (str.length % 2 !== 0) { |
|
|
throw new Error("Invalid hex string length."); |
|
|
throw new Error("Invalid hex string length."); |
|
|
} |
|
|
} |
|
|
let bytes = new Uint8Array(str.length / 2); |
|
|
let bytes = new Uint8Array(str.length / 2); |
|
|
for (let i = 0; i < str.length; i += 2) { |
|
|
for (let i = 0; i < str.length; i += 2) { |
|
|
bytes[i / 2] = parseInt(str.substring(i, i + 2), 16); |
|
|
bytes[i / 2] = parseInt(str.substring(i, i + 2), 16); |
|
|
} |
|
|
|
|
|
return bytes; |
|
|
|
|
|
} else if (encoding === "base64url") { |
|
|
|
|
|
str = str.replace(/-/g, "+").replace(/_/g, "/"); |
|
|
|
|
|
while (str.length % 4) { |
|
|
|
|
|
str += "="; |
|
|
|
|
|
} |
|
|
|
|
|
return new Uint8Array(bufferFromBase64(str)); |
|
|
|
|
|
} else { |
|
|
|
|
|
throw new Error(`Unsupported encoding "${encoding}"`); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
return bytes; |
|
|
|
|
|
} else if (encoding === "base64url") { |
|
|
|
|
|
str = str.replace(/-/g, "+").replace(/_/g, "/"); |
|
|
|
|
|
while (str.length % 4) { |
|
|
|
|
|
str += "="; |
|
|
|
|
|
} |
|
|
|
|
|
return new Uint8Array(bufferFromBase64(str)); |
|
|
|
|
|
} else { |
|
|
|
|
|
throw new Error(`Unsupported encoding "${encoding}"`); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
@ -43,16 +42,16 @@ function fromString(str, encoding = "utf8") { |
|
|
* @throws {Error} - Throws an error if the encoding is unsupported. |
|
|
* @throws {Error} - Throws an error if the encoding is unsupported. |
|
|
*/ |
|
|
*/ |
|
|
function toString(byteArray, encoding = "utf8") { |
|
|
function toString(byteArray, encoding = "utf8") { |
|
|
switch (encoding) { |
|
|
switch (encoding) { |
|
|
case "utf8": |
|
|
case "utf8": |
|
|
return decodeUTF8(byteArray); |
|
|
return decodeUTF8(byteArray); |
|
|
case "base16": |
|
|
case "base16": |
|
|
return toBase16(byteArray); |
|
|
return toBase16(byteArray); |
|
|
case "base64url": |
|
|
case "base64url": |
|
|
return toBase64Url(byteArray); |
|
|
return toBase64Url(byteArray); |
|
|
default: |
|
|
default: |
|
|
throw new Error(`Unsupported encoding "${encoding}"`); |
|
|
throw new Error(`Unsupported encoding "${encoding}"`); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
@ -62,7 +61,7 @@ function toString(byteArray, encoding = "utf8") { |
|
|
* @returns {string} |
|
|
* @returns {string} |
|
|
*/ |
|
|
*/ |
|
|
function decodeUTF8(byteArray) { |
|
|
function decodeUTF8(byteArray) { |
|
|
return new TextDecoder().decode(byteArray); |
|
|
return new TextDecoder().decode(byteArray); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
@ -72,9 +71,9 @@ function decodeUTF8(byteArray) { |
|
|
* @returns {string} |
|
|
* @returns {string} |
|
|
*/ |
|
|
*/ |
|
|
function toBase16(byteArray) { |
|
|
function toBase16(byteArray) { |
|
|
return Array.from(byteArray) |
|
|
return Array.from(byteArray) |
|
|
.map((byte) => byte.toString(16).padStart(2, "0")) |
|
|
.map((byte) => byte.toString(16).padStart(2, "0")) |
|
|
.join(""); |
|
|
.join(""); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
@ -84,475 +83,462 @@ function toBase16(byteArray) { |
|
|
* @returns {string} |
|
|
* @returns {string} |
|
|
*/ |
|
|
*/ |
|
|
function toBase64Url(byteArray) { |
|
|
function toBase64Url(byteArray) { |
|
|
let uint8Array = new Uint8Array(byteArray); |
|
|
let uint8Array = new Uint8Array(byteArray); |
|
|
let binaryString = ""; |
|
|
let binaryString = ""; |
|
|
for (let i = 0; i < uint8Array.length; i++) { |
|
|
for (let i = 0; i < uint8Array.length; i++) { |
|
|
binaryString += String.fromCharCode(uint8Array[i]); |
|
|
binaryString += String.fromCharCode(uint8Array[i]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Encode to base64
|
|
|
// Encode to base64
|
|
|
let base64 = btoa(binaryString); |
|
|
let base64 = btoa(binaryString); |
|
|
|
|
|
|
|
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); |
|
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const u8a = { toString, fromString }; |
|
|
const u8a = { toString, fromString }; |
|
|
|
|
|
|
|
|
function sha256(payload) { |
|
|
function sha256(payload) { |
|
|
const data = typeof payload === "string" ? u8a.fromString(payload) : payload; |
|
|
const data = typeof payload === "string" ? u8a.fromString(payload) : payload; |
|
|
return nobleHashes.sha256(data); |
|
|
return nobleHashes.sha256(data); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function accessToken(identifier) { |
|
|
async function accessToken(identifier) { |
|
|
const did = identifier["did"]; |
|
|
const did = identifier["did"]; |
|
|
const privateKeyHex = identifier["keys"][0]["privateKeyHex"]; |
|
|
const privateKeyHex = identifier["keys"][0]["privateKeyHex"]; |
|
|
|
|
|
|
|
|
const signer = await SimpleSigner(privateKeyHex); |
|
|
const signer = await SimpleSigner(privateKeyHex); |
|
|
|
|
|
|
|
|
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 }; |
|
|
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
|
|
|
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
|
|
|
const jwt = await createJWT(tokenPayload, { |
|
|
const jwt = await createJWT(tokenPayload, { |
|
|
alg, |
|
|
alg, |
|
|
issuer: did, |
|
|
issuer: did, |
|
|
signer, |
|
|
signer, |
|
|
}); |
|
|
}); |
|
|
console.error(jwt); |
|
|
return jwt; |
|
|
return jwt; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function createJWT(payload, options, header = {}) { |
|
|
async function createJWT(payload, options, header = {}) { |
|
|
const { issuer, signer, alg, expiresIn, canonicalize } = options; |
|
|
const { issuer, signer, alg, expiresIn, canonicalize } = options; |
|
|
|
|
|
|
|
|
if (!signer) |
|
|
if (!signer) |
|
|
throw new Error( |
|
|
throw new Error( |
|
|
"missing_signer: No Signer functionality has been configured", |
|
|
"missing_signer: No Signer functionality has been configured", |
|
|
); |
|
|
); |
|
|
if (!issuer) |
|
|
if (!issuer) |
|
|
throw new Error("missing_issuer: No issuing DID has been configured"); |
|
|
throw new Error("missing_issuer: No issuing DID has been configured"); |
|
|
if (!header.typ) header.typ = "JWT"; |
|
|
if (!header.typ) header.typ = "JWT"; |
|
|
if (!header.alg) header.alg = alg; |
|
|
if (!header.alg) header.alg = alg; |
|
|
|
|
|
|
|
|
const timestamps = { |
|
|
const timestamps = { |
|
|
iat: Math.floor(Date.now() / 1000), |
|
|
iat: Math.floor(Date.now() / 1000), |
|
|
exp: undefined, |
|
|
exp: undefined, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
if (expiresIn) { |
|
|
if (expiresIn) { |
|
|
if (typeof expiresIn === "number") { |
|
|
if (typeof expiresIn === "number") { |
|
|
timestamps.exp = (payload.nbf || timestamps.iat) + Math.floor(expiresIn); |
|
|
timestamps.exp = (payload.nbf || timestamps.iat) + Math.floor(expiresIn); |
|
|
} else { |
|
|
} else { |
|
|
throw new Error("invalid_argument: JWT expiresIn is not a number"); |
|
|
throw new Error("invalid_argument: JWT expiresIn is not a number"); |
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const fullPayload = { ...timestamps, ...payload, iss: issuer }; |
|
|
const fullPayload = { ...timestamps, ...payload, iss: issuer }; |
|
|
return createJWS(fullPayload, signer, header, { canonicalize }); |
|
|
return createJWS(fullPayload, signer, header, { canonicalize }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const defaultAlg = "ES256K"; |
|
|
const defaultAlg = "ES256K"; |
|
|
|
|
|
|
|
|
async function createJWS(payload, signer, header = {}, options = {}) { |
|
|
async function createJWS(payload, signer, header = {}, options = {}) { |
|
|
if (!header.alg) header.alg = defaultAlg; |
|
|
if (!header.alg) header.alg = defaultAlg; |
|
|
const encodedPayload = |
|
|
const encodedPayload = |
|
|
typeof payload === "string" |
|
|
typeof payload === "string" |
|
|
? payload |
|
|
? payload |
|
|
: encodeSection(payload, options.canonicalize); |
|
|
: encodeSection(payload, options.canonicalize); |
|
|
const signingInput = [ |
|
|
const signingInput = [ |
|
|
encodeSection(header, options.canonicalize), |
|
|
encodeSection(header, options.canonicalize), |
|
|
encodedPayload, |
|
|
encodedPayload, |
|
|
].join("."); |
|
|
].join("."); |
|
|
|
|
|
|
|
|
const jwtSigner = ES256KSignerAlg(false); |
|
|
const jwtSigner = ES256KSignerAlg(false); |
|
|
const signature = await jwtSigner(signingInput, signer); |
|
|
const signature = await jwtSigner(signingInput, signer); |
|
|
|
|
|
|
|
|
// JWS Compact Serialization
|
|
|
// JWS Compact Serialization
|
|
|
// https://www.rfc-editor.org/rfc/rfc7515#section-7.1
|
|
|
// https://www.rfc-editor.org/rfc/rfc7515#section-7.1
|
|
|
return [signingInput, signature].join("."); |
|
|
return [signingInput, signature].join("."); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function canonicalizeData(object) { |
|
|
function canonicalizeData(object) { |
|
|
if (typeof object === "number" && isNaN(object)) { |
|
|
if (typeof object === "number" && isNaN(object)) { |
|
|
throw new Error("NaN is not allowed"); |
|
|
throw new Error("NaN is not allowed"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof object === "number" && !isFinite(object)) { |
|
|
if (typeof object === "number" && !isFinite(object)) { |
|
|
throw new Error("Infinity is not allowed"); |
|
|
throw new Error("Infinity is not allowed"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (object === null || typeof object !== "object") { |
|
|
if (object === null || typeof object !== "object") { |
|
|
return JSON.stringify(object); |
|
|
return JSON.stringify(object); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (object.toJSON instanceof Function) { |
|
|
if (object.toJSON instanceof Function) { |
|
|
return serialize(object.toJSON()); |
|
|
return serialize(object.toJSON()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (Array.isArray(object)) { |
|
|
if (Array.isArray(object)) { |
|
|
const values = object.reduce((t, cv, ci) => { |
|
|
const values = object.reduce((t, cv, ci) => { |
|
|
const comma = ci === 0 ? "" : ","; |
|
|
const comma = ci === 0 ? "" : ","; |
|
|
const value = cv === undefined || typeof cv === "symbol" ? null : cv; |
|
|
const value = cv === undefined || typeof cv === "symbol" ? null : cv; |
|
|
return `${t}${comma}${serialize(value)}`; |
|
|
return `${t}${comma}${serialize(value)}`; |
|
|
}, ""); |
|
|
}, ""); |
|
|
return `[${values}]`; |
|
|
return `[${values}]`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const values = Object.keys(object) |
|
|
const values = Object.keys(object) |
|
|
.sort() |
|
|
.sort() |
|
|
.reduce((t, cv) => { |
|
|
.reduce((t, cv) => { |
|
|
if (object[cv] === undefined || typeof object[cv] === "symbol") { |
|
|
if (object[cv] === undefined || typeof object[cv] === "symbol") { |
|
|
return t; |
|
|
return t; |
|
|
} |
|
|
} |
|
|
const comma = t.length === 0 ? "" : ","; |
|
|
const comma = t.length === 0 ? "" : ","; |
|
|
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`; |
|
|
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`; |
|
|
}, ""); |
|
|
}, ""); |
|
|
return `{${values}}`; |
|
|
return `{${values}}`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function encodeSection(data, shouldCanonicalize = false) { |
|
|
function encodeSection(data, shouldCanonicalize = false) { |
|
|
if (shouldCanonicalize) { |
|
|
if (shouldCanonicalize) { |
|
|
return encodeBase64url(canonicalizeData(data)); |
|
|
return encodeBase64url(canonicalizeData(data)); |
|
|
} else { |
|
|
} else { |
|
|
return encodeBase64url(JSON.stringify(data)); |
|
|
return encodeBase64url(JSON.stringify(data)); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function encodeBase64url(s) { |
|
|
function encodeBase64url(s) { |
|
|
return bytesToBase64url(u8a.fromString(s)); |
|
|
return bytesToBase64url(u8a.fromString(s)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function instanceOfEcdsaSignature(object) { |
|
|
function instanceOfEcdsaSignature(object) { |
|
|
return typeof object === "object" && "r" in object && "s" in object; |
|
|
return typeof object === "object" && "r" in object && "s" in object; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function ES256KSignerAlg(recoverable) { |
|
|
function ES256KSignerAlg(recoverable) { |
|
|
return async function sign(payload, signer) { |
|
|
return async function sign(payload, signer) { |
|
|
const signature = await signer(payload); |
|
|
const signature = await signer(payload); |
|
|
if (instanceOfEcdsaSignature(signature)) { |
|
|
if (instanceOfEcdsaSignature(signature)) { |
|
|
return toJose(signature, recoverable); |
|
|
return toJose(signature, recoverable); |
|
|
} else { |
|
|
} else { |
|
|
if ( |
|
|
if ( |
|
|
recoverable && |
|
|
recoverable && |
|
|
typeof fromJose(signature).recoveryParam === "undefined" |
|
|
typeof fromJose(signature).recoveryParam === "undefined" |
|
|
) { |
|
|
) { |
|
|
throw new Error( |
|
|
throw new Error( |
|
|
`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`, |
|
|
`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
return signature; |
|
|
return signature; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function leftpad(data, size = 64) { |
|
|
function leftpad(data, size = 64) { |
|
|
if (data.length === size) return data; |
|
|
if (data.length === size) return data; |
|
|
return "0".repeat(size - data.length) + data; |
|
|
return "0".repeat(size - data.length) + data; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function SimpleSigner(hexPrivateKey) { |
|
|
async function SimpleSigner(hexPrivateKey) { |
|
|
const signer = await ES256KSigner(hexToBytes(hexPrivateKey), true); |
|
|
const signer = await ES256KSigner(hexToBytes(hexPrivateKey), true); |
|
|
return async (data) => { |
|
|
return async (data) => { |
|
|
const signature = await signer(data); |
|
|
const signature = await signer(data); |
|
|
return fromJose(signature); |
|
|
return fromJose(signature); |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function hexToBytes(s, minLength) { |
|
|
function hexToBytes(s, minLength) { |
|
|
let input = s.startsWith("0x") ? s.substring(2) : s; |
|
|
let input = s.startsWith("0x") ? s.substring(2) : s; |
|
|
|
|
|
|
|
|
if (input.length % 2 !== 0) { |
|
|
if (input.length % 2 !== 0) { |
|
|
input = `0${input}`; |
|
|
input = `0${input}`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (minLength) { |
|
|
if (minLength) { |
|
|
const paddedLength = Math.max(input.length, minLength * 2); |
|
|
const paddedLength = Math.max(input.length, minLength * 2); |
|
|
input = input.padStart(paddedLength, "00"); |
|
|
input = input.padStart(paddedLength, "00"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return u8a.fromString(input.toLowerCase(), "base16"); |
|
|
return u8a.fromString(input.toLowerCase(), "base16"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function ES256KSigner(privateKey, recoverable = false) { |
|
|
function ES256KSigner(privateKey, recoverable = false) { |
|
|
const privateKeyBytes = privateKey; |
|
|
const privateKeyBytes = privateKey; |
|
|
if (privateKeyBytes.length !== 32) { |
|
|
if (privateKeyBytes.length !== 32) { |
|
|
throw new Error( |
|
|
throw new Error( |
|
|
`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`, |
|
|
`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return async function (data) { |
|
|
return async function (data) { |
|
|
const signature = nobleCurves.secp256k1.sign(sha256(data), privateKeyBytes); |
|
|
const signature = nobleCurves.secp256k1.sign(sha256(data), privateKeyBytes); |
|
|
console.error(signature); |
|
|
return toJose( |
|
|
return toJose( |
|
|
{ |
|
|
{ |
|
|
r: leftpad(signature.r.toString(16)), |
|
|
r: leftpad(signature.r.toString(16)), |
|
|
s: leftpad(signature.s.toString(16)), |
|
|
s: leftpad(signature.s.toString(16)), |
|
|
recoveryParam: signature.recovery, |
|
|
recoveryParam: signature.recovery, |
|
|
}, |
|
|
}, |
|
|
recoverable, |
|
|
recoverable, |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function toJose(signature, recoverable) { |
|
|
function toJose(signature, recoverable) { |
|
|
const { r, s, recoveryParam } = signature; |
|
|
const { r, s, recoveryParam } = signature; |
|
|
const jose = new Uint8Array(recoverable ? 65 : 64); |
|
|
const jose = new Uint8Array(recoverable ? 65 : 64); |
|
|
jose.set(u8a.fromString(r, "base16"), 0); |
|
|
jose.set(u8a.fromString(r, "base16"), 0); |
|
|
jose.set(u8a.fromString(s, "base16"), 32); |
|
|
jose.set(u8a.fromString(s, "base16"), 32); |
|
|
|
|
|
|
|
|
if (recoverable) { |
|
|
if (recoverable) { |
|
|
if (typeof recoveryParam === "undefined") { |
|
|
if (typeof recoveryParam === "undefined") { |
|
|
throw new Error("Signer did not return a recoveryParam"); |
|
|
throw new Error("Signer did not return a recoveryParam"); |
|
|
} |
|
|
|
|
|
jose[64] = recoveryParam; |
|
|
|
|
|
} |
|
|
} |
|
|
return bytesToBase64url(jose); |
|
|
jose[64] = recoveryParam; |
|
|
|
|
|
} |
|
|
|
|
|
return bytesToBase64url(jose); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function bytesToBase64url(b) { |
|
|
function bytesToBase64url(b) { |
|
|
return u8a.toString(b, "base64url"); |
|
|
return u8a.toString(b, "base64url"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function base64ToBytes(s) { |
|
|
function base64ToBytes(s) { |
|
|
const inputBase64Url = s |
|
|
const inputBase64Url = s |
|
|
.replace(/\+/g, "-") |
|
|
.replace(/\+/g, "-") |
|
|
.replace(/\//g, "_") |
|
|
.replace(/\//g, "_") |
|
|
.replace(/=/g, ""); |
|
|
.replace(/=/g, ""); |
|
|
return u8a.fromString(inputBase64Url, "base64url"); |
|
|
return u8a.fromString(inputBase64Url, "base64url"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function bytesToHex(b) { |
|
|
function bytesToHex(b) { |
|
|
return u8a.toString(b, "base16"); |
|
|
return u8a.toString(b, "base16"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function fromJose(signature) { |
|
|
function fromJose(signature) { |
|
|
const signatureBytes = base64ToBytes(signature); |
|
|
const signatureBytes = base64ToBytes(signature); |
|
|
if (signatureBytes.length < 64 || signatureBytes.length > 65) { |
|
|
if (signatureBytes.length < 64 || signatureBytes.length > 65) { |
|
|
throw new TypeError( |
|
|
throw new TypeError( |
|
|
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`, |
|
|
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
const r = bytesToHex(signatureBytes.slice(0, 32)); |
|
|
const r = bytesToHex(signatureBytes.slice(0, 32)); |
|
|
const s = bytesToHex(signatureBytes.slice(32, 64)); |
|
|
const s = bytesToHex(signatureBytes.slice(32, 64)); |
|
|
const recoveryParam = |
|
|
const recoveryParam = |
|
|
signatureBytes.length === 65 ? signatureBytes[64] : undefined; |
|
|
signatureBytes.length === 65 ? signatureBytes[64] : undefined; |
|
|
|
|
|
|
|
|
return { r, s, recoveryParam }; |
|
|
return { r, s, recoveryParam }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function validateBase64(s) { |
|
|
function validateBase64(s) { |
|
|
if ( |
|
|
if ( |
|
|
!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test( |
|
|
!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test( |
|
|
s, |
|
|
s, |
|
|
) |
|
|
) |
|
|
) { |
|
|
) { |
|
|
throw new TypeError("invalid encoding"); |
|
|
throw new TypeError("invalid encoding"); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function decodeBase64(s) { |
|
|
function decodeBase64(s) { |
|
|
validateBase64(s); |
|
|
validateBase64(s); |
|
|
var i, |
|
|
var i, |
|
|
d = atob(s), |
|
|
d = atob(s), |
|
|
b = new Uint8Array(d.length); |
|
|
b = new Uint8Array(d.length); |
|
|
for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); |
|
|
for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); |
|
|
return b; |
|
|
return b; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function getSettingById(id) { |
|
|
async function getSettingById(id) { |
|
|
return new Promise((resolve, reject) => { |
|
|
return new Promise((resolve, reject) => { |
|
|
let openRequest = indexedDB.open("TimeSafari"); |
|
|
let openRequest = indexedDB.open("TimeSafari"); |
|
|
|
|
|
|
|
|
openRequest.onupgradeneeded = (event) => { |
|
|
openRequest.onupgradeneeded = (event) => { |
|
|
// Handle database setup if necessary
|
|
|
// Handle database setup if necessary
|
|
|
let db = event.target.result; |
|
|
let db = event.target.result; |
|
|
if (!db.objectStoreNames.contains("settings")) { |
|
|
if (!db.objectStoreNames.contains("settings")) { |
|
|
db.createObjectStore("settings", { keyPath: "id" }); |
|
|
db.createObjectStore("settings", { keyPath: "id" }); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
openRequest.onsuccess = (event) => { |
|
|
|
|
|
let db = event.target.result; |
|
|
|
|
|
let transaction = db.transaction("settings", "readonly"); |
|
|
|
|
|
let objectStore = transaction.objectStore("settings"); |
|
|
|
|
|
let getRequest = objectStore.get(id); |
|
|
|
|
|
|
|
|
|
|
|
getRequest.onsuccess = () => resolve(getRequest.result); |
|
|
|
|
|
getRequest.onerror = () => reject(getRequest.error); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
openRequest.onerror = () => reject(openRequest.error); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function setMostRecentNotified(id) { |
|
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
|
|
|
|
|
|
|
|
const dbName = "TimeSafari"; |
|
|
|
|
|
const storeName = "settings"; |
|
|
|
|
|
const key = 1; |
|
|
|
|
|
const propertyToUpdate = "lastNotifiedClaimId"; |
|
|
|
|
|
const newValue = id; |
|
|
|
|
|
|
|
|
|
|
|
let request = indexedDB.open(dbName); |
|
|
|
|
|
|
|
|
|
|
|
request.onerror = function(event) { |
|
|
|
|
|
console.error("Database error: " + event.target.errorCode); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
request.onsuccess = function(event) { |
|
|
|
|
|
let db = event.target.result; |
|
|
|
|
|
let transaction = db.transaction(storeName, "readwrite"); |
|
|
|
|
|
let store = transaction.objectStore(storeName); |
|
|
|
|
|
|
|
|
|
|
|
let getRequest = store.get(key); |
|
|
openRequest.onsuccess = (event) => { |
|
|
|
|
|
let db = event.target.result; |
|
|
|
|
|
let transaction = db.transaction("settings", "readonly"); |
|
|
|
|
|
let objectStore = transaction.objectStore("settings"); |
|
|
|
|
|
let getRequest = objectStore.get(id); |
|
|
|
|
|
|
|
|
getRequest.onsuccess = function(event) { |
|
|
getRequest.onsuccess = () => resolve(getRequest.result); |
|
|
let data = event.target.result; |
|
|
getRequest.onerror = () => reject(getRequest.error); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// Only update if the object exists
|
|
|
openRequest.onerror = () => reject(openRequest.error); |
|
|
if (data) { |
|
|
}); |
|
|
// Update the specific property
|
|
|
} |
|
|
data[propertyToUpdate] = newValue; |
|
|
|
|
|
|
|
|
|
|
|
// Put the updated object back into the database
|
|
|
|
|
|
let putRequest = store.put(data); |
|
|
|
|
|
|
|
|
|
|
|
putRequest.onsuccess = function() { |
|
|
async function setMostRecentNotified(id) { |
|
|
console.error("Record updated successfully"); |
|
|
try { |
|
|
}; |
|
|
const db = await openIndexedDB("TimeSafari"); |
|
|
|
|
|
const transaction = db.transaction("settings", "readwrite"); |
|
|
|
|
|
const store = transaction.objectStore("settings"); |
|
|
|
|
|
|
|
|
|
|
|
const data = await getRecord(store, 1); |
|
|
|
|
|
if (data) { |
|
|
|
|
|
data["lastNotifiedClaimId"] = id; |
|
|
|
|
|
await updateRecord(store, data); |
|
|
|
|
|
} else { |
|
|
|
|
|
console.error("Record not found"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
transaction.oncomplete = () => db.close(); |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error("Database error: " + error.message); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
putRequest.onerror = function(event) { |
|
|
|
|
|
console.error("Error updating record: " + event.target.errorCode); |
|
|
|
|
|
}; |
|
|
|
|
|
} else { |
|
|
|
|
|
console.error("Record not found"); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
getRequest.onerror = function(event) { |
|
|
function openIndexedDB(dbName) { |
|
|
console.error("Error fetching record: " + event.target.errorCode); |
|
|
return new Promise((resolve, reject) => { |
|
|
}; |
|
|
const request = indexedDB.open(dbName); |
|
|
|
|
|
request.onerror = () => reject(request.error); |
|
|
|
|
|
request.onsuccess = () => resolve(request.result); |
|
|
|
|
|
request.onupgradeneeded = (event) => { |
|
|
|
|
|
const db = event.target.result; |
|
|
|
|
|
if (!db.objectStoreNames.contains("settings")) { |
|
|
|
|
|
db.createObjectStore("settings"); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
transaction.oncomplete = function() { |
|
|
|
|
|
db.close(); |
|
|
|
|
|
}; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
request.onupgradeneeded = function(event) { |
|
|
function getRecord(store, key) { |
|
|
let db = event.target.result; |
|
|
return new Promise((resolve, reject) => { |
|
|
if (!db.objectStoreNames.contains(storeName)) { |
|
|
const request = store.get(key); |
|
|
db.createObjectStore(storeName); |
|
|
request.onsuccess = () => resolve(request.result); |
|
|
} |
|
|
request.onerror = () => reject(request.error); |
|
|
}; |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateRecord(store, data) { |
|
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
|
|
const request = store.put(data); |
|
|
|
|
|
request.onsuccess = () => resolve(request.result); |
|
|
|
|
|
request.onerror = () => reject(request.error); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchAllAccounts() { |
|
|
async function fetchAllAccounts() { |
|
|
return new Promise((resolve, reject) => { |
|
|
return new Promise((resolve, reject) => { |
|
|
let openRequest = indexedDB.open("TimeSafariAccounts"); |
|
|
let openRequest = indexedDB.open("TimeSafariAccounts"); |
|
|
|
|
|
|
|
|
openRequest.onupgradeneeded = function (event) { |
|
|
openRequest.onupgradeneeded = function (event) { |
|
|
let db = event.target.result; |
|
|
let db = event.target.result; |
|
|
if (!db.objectStoreNames.contains("accounts")) { |
|
|
if (!db.objectStoreNames.contains("accounts")) { |
|
|
db.createObjectStore("accounts", { keyPath: "id" }); |
|
|
db.createObjectStore("accounts", { keyPath: "id" }); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
openRequest.onsuccess = function (event) { |
|
|
openRequest.onsuccess = function (event) { |
|
|
let db = event.target.result; |
|
|
let db = event.target.result; |
|
|
let transaction = db.transaction("accounts", "readonly"); |
|
|
let transaction = db.transaction("accounts", "readonly"); |
|
|
let objectStore = transaction.objectStore("accounts"); |
|
|
let objectStore = transaction.objectStore("accounts"); |
|
|
let getAllRequest = objectStore.getAll(); |
|
|
let getAllRequest = objectStore.getAll(); |
|
|
|
|
|
|
|
|
getAllRequest.onsuccess = function () { |
|
|
getAllRequest.onsuccess = function () { |
|
|
resolve(getAllRequest.result); |
|
|
resolve(getAllRequest.result); |
|
|
}; |
|
|
}; |
|
|
getAllRequest.onerror = function () { |
|
|
getAllRequest.onerror = function () { |
|
|
reject(getAllRequest.error); |
|
|
reject(getAllRequest.error); |
|
|
}; |
|
|
}; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
openRequest.onerror = function () { |
|
|
openRequest.onerror = function () { |
|
|
reject(openRequest.error); |
|
|
reject(openRequest.error); |
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function getNotificationCount() { |
|
|
async function getNotificationCount() { |
|
|
let secret = null; |
|
|
let secret = null; |
|
|
let accounts = []; |
|
|
let accounts = []; |
|
|
let result = null; |
|
|
let result = null; |
|
|
if ("secret" in self) { |
|
|
if ("secret" in self) { |
|
|
secret = self.secret; |
|
|
secret = self.secret; |
|
|
const secretUint8Array = self.decodeBase64(secret); |
|
|
const secretUint8Array = self.decodeBase64(secret); |
|
|
const settings = await getSettingById(1); |
|
|
const settings = await getSettingById(1); |
|
|
const activeDid = settings["activeDid"]; |
|
|
let lastNotifiedClaimId = null; |
|
|
accounts = await fetchAllAccounts(); |
|
|
if ("lastNotifiedClaimId" in settings) { |
|
|
let did = null; |
|
|
lastNotifiedClaimId = settings["lastNotifiedClaimId"]; |
|
|
for (var i = 0; i < accounts.length; i++) { |
|
|
} |
|
|
let account = accounts[i]; |
|
|
const activeDid = settings["activeDid"]; |
|
|
let did = account["did"]; |
|
|
accounts = await fetchAllAccounts(); |
|
|
if (did == activeDid) { |
|
|
let did = null; |
|
|
let publicKeyHex = account["publicKeyHex"]; |
|
|
for (var i = 0; i < accounts.length; i++) { |
|
|
let identity = account["identity"]; |
|
|
let account = accounts[i]; |
|
|
const messageWithNonceAsUint8Array = self.decodeBase64(identity); |
|
|
let did = account["did"]; |
|
|
const nonce = messageWithNonceAsUint8Array.slice(0, 24); |
|
|
if (did == activeDid) { |
|
|
const message = messageWithNonceAsUint8Array.slice(24, identity.length); |
|
|
let publicKeyHex = account["publicKeyHex"]; |
|
|
const decoder = new TextDecoder("utf-8"); |
|
|
let identity = account["identity"]; |
|
|
const decrypted = self.secretbox.open(message, nonce, secretUint8Array); |
|
|
const messageWithNonceAsUint8Array = self.decodeBase64(identity); |
|
|
|
|
|
const nonce = messageWithNonceAsUint8Array.slice(0, 24); |
|
|
const msg = decoder.decode(decrypted); |
|
|
const message = messageWithNonceAsUint8Array.slice(24, identity.length); |
|
|
const identifier = JSON.parse(JSON.parse(msg)); |
|
|
const decoder = new TextDecoder("utf-8"); |
|
|
|
|
|
const decrypted = self.secretbox.open(message, nonce, secretUint8Array); |
|
|
console.log(identifier); |
|
|
|
|
|
|
|
|
const msg = decoder.decode(decrypted); |
|
|
const headers = { |
|
|
const identifier = JSON.parse(JSON.parse(msg)); |
|
|
"Content-Type": "application/json", |
|
|
|
|
|
}; |
|
|
const headers = { |
|
|
|
|
|
"Content-Type": "application/json", |
|
|
headers["Authorization"] = "Bearer " + (await accessToken(identifier)); |
|
|
}; |
|
|
|
|
|
|
|
|
let response = await fetch( |
|
|
headers["Authorization"] = "Bearer " + (await accessToken(identifier)); |
|
|
"https://test-api.endorser.ch/api/v2/report/claims", |
|
|
|
|
|
{ |
|
|
let response = await fetch( |
|
|
method: "GET", |
|
|
"https://test-api.endorser.ch/api/v2/report/claims", |
|
|
headers: headers, |
|
|
{ |
|
|
}, |
|
|
method: "GET", |
|
|
); |
|
|
headers: headers, |
|
|
console.error(did, response.status); |
|
|
}, |
|
|
const claims = await response.json(); |
|
|
); |
|
|
// formulate a message back for notifications
|
|
|
if (response.status == 200) { |
|
|
let lastNotifiedClaimId = null; |
|
|
let json = await response.json(); |
|
|
if ('lastNotifiedClaimId' in settings) { |
|
|
let claims = json["data"]; |
|
|
lastNotifiedClaimId = settings['lastNotifiedClaimId']; |
|
|
let newClaims = 0; |
|
|
} |
|
|
for (var i = 0; i < claims.length; i++) { |
|
|
let newClaims = 0; |
|
|
let claim = claims[i]; |
|
|
// search for id recent notified -- if it exists if not return everything count
|
|
|
if (claim["id"] === lastNotifiedClaimId) { |
|
|
for (var i=0; i< response.json()['data'].length; i++) { |
|
|
break; |
|
|
let claim = response.json()['data']; |
|
|
} |
|
|
if (claim['id'] == lastNotifiedClaimId) { |
|
|
newClaims++; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (newClaims === 0) { |
|
|
newClaims++; |
|
|
result = "You have no new claims today."; |
|
|
} |
|
|
} else { |
|
|
// make the notification message here
|
|
|
result = `${newClaims} have been shared with you`; |
|
|
|
|
|
} |
|
|
// first claim is the most recent (?)
|
|
|
const most_recent_notified = claims[0]["id"]; |
|
|
const most_recent_notified = claims[0]['id']; |
|
|
await setMostRecentNotified(most_recent_notified); |
|
|
await setMostRecentNotified(most_recent_notified); |
|
|
return "TEST"; |
|
|
|
|
|
} else { |
|
|
|
|
|
console.error(response.status); |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return result; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
self.getNotificationCount = getNotificationCount; |
|
|
self.getNotificationCount = getNotificationCount; |
|
|