forked from trent_larson/crowd-funder-for-time-pwa
able to sign -- let's see what happens next
This commit is contained in:
@@ -8,6 +8,337 @@ async function generateSHA256Hash(data) {
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
function bufferFromBase64(base64) {
|
||||
const binaryString = atob(base64);
|
||||
const length = binaryString.length;
|
||||
const bytes = new Uint8Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function fromString(str, encoding = 'utf8') {
|
||||
if (encoding === 'utf8') {
|
||||
return new TextEncoder().encode(str);
|
||||
} else if (encoding === 'base16') {
|
||||
if (str.length % 2 !== 0) {
|
||||
throw new Error('Invalid hex string length.');
|
||||
}
|
||||
let bytes = new Uint8Array(str.length / 2);
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a Uint8Array to a string with the given encoding.
|
||||
*
|
||||
* @param {Uint8Array} byteArray - The Uint8Array to convert.
|
||||
* @param {string} [encoding='utf8'] - The desired encoding ('utf8', 'base16', 'base64url').
|
||||
* @returns {string} - The encoded string.
|
||||
* @throws {Error} - Throws an error if the encoding is unsupported.
|
||||
*/
|
||||
function toString(byteArray, encoding = 'utf8') {
|
||||
switch (encoding) {
|
||||
case 'utf8':
|
||||
return decodeUTF8(byteArray);
|
||||
case 'base16':
|
||||
return toBase16(byteArray);
|
||||
case 'base64url':
|
||||
return toBase64Url(byteArray);
|
||||
default:
|
||||
throw new Error(`Unsupported encoding "${encoding}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a Uint8Array as a UTF-8 string.
|
||||
*
|
||||
* @param {Uint8Array} byteArray
|
||||
* @returns {string}
|
||||
*/
|
||||
function decodeUTF8(byteArray) {
|
||||
return new TextDecoder().decode(byteArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Uint8Array to a base16 (hex) encoded string.
|
||||
*
|
||||
* @param {Uint8Array} byteArray
|
||||
* @returns {string}
|
||||
*/
|
||||
function toBase16(byteArray) {
|
||||
return Array.from(byteArray).map(byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Uint8Array to a base64url encoded string.
|
||||
*
|
||||
* @param {Uint8Array} byteArray
|
||||
* @returns {string}
|
||||
*/
|
||||
function toBase64Url(byteArray) {
|
||||
let uint8Array = new Uint8Array(byteArray);
|
||||
let binaryString = '';
|
||||
for (let i = 0; i < uint8Array.length; i++) {
|
||||
binaryString += String.fromCharCode(uint8Array[i]);
|
||||
}
|
||||
|
||||
// Encode to base64
|
||||
let base64 = btoa(binaryString);
|
||||
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
const u8a = { toString, fromString };
|
||||
|
||||
function sha256(payload) {
|
||||
const data = typeof payload === 'string' ? u8a.fromString(payload) : payload;
|
||||
return generateSHA256Hash(data);
|
||||
}
|
||||
|
||||
|
||||
async function accessToken(identifier) {
|
||||
const did = identifier['did'];
|
||||
const privateKeyHex = identifier['keys'][0]['privateKeyHex'];
|
||||
|
||||
const signer = await SimpleSigner(privateKeyHex);
|
||||
|
||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||
const endEpoch = nowEpoch + 60; // add one minute
|
||||
|
||||
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
||||
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
|
||||
const jwt = await createJWT(tokenPayload, {
|
||||
alg,
|
||||
issuer: did,
|
||||
signer,
|
||||
});
|
||||
return jwt;
|
||||
}
|
||||
|
||||
|
||||
async function createJWT(payload, options, header = {}) {
|
||||
const { issuer, signer, alg, expiresIn, canonicalize } = options;
|
||||
|
||||
if (!signer) throw new Error('missing_signer: No Signer functionality has been configured');
|
||||
if (!issuer) throw new Error('missing_issuer: No issuing DID has been configured');
|
||||
if (!header.typ) header.typ = 'JWT';
|
||||
if (!header.alg) header.alg = alg;
|
||||
|
||||
const timestamps = {
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: undefined,
|
||||
};
|
||||
|
||||
if (expiresIn) {
|
||||
if (typeof expiresIn === 'number') {
|
||||
timestamps.exp = (payload.nbf || timestamps.iat) + Math.floor(expiresIn);
|
||||
} else {
|
||||
throw new Error('invalid_argument: JWT expiresIn is not a number');
|
||||
}
|
||||
}
|
||||
|
||||
const fullPayload = { ...timestamps, ...payload, iss: issuer };
|
||||
return createJWS(fullPayload, signer, header, { canonicalize });
|
||||
}
|
||||
|
||||
|
||||
const defaultAlg = 'ES256K'
|
||||
|
||||
|
||||
async function createJWS(payload, signer, header = {}, options = {}) {
|
||||
if (!header.alg) header.alg = defaultAlg;
|
||||
const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload, options.canonicalize);
|
||||
const signingInput = [encodeSection(header, options.canonicalize), encodedPayload].join('.');
|
||||
|
||||
const jwtSigner = ES256KSignerAlg(false);
|
||||
const signature = await jwtSigner(signingInput, signer);
|
||||
|
||||
// JWS Compact Serialization
|
||||
// https://www.rfc-editor.org/rfc/rfc7515#section-7.1
|
||||
return [signingInput, signature].join('.');
|
||||
}
|
||||
|
||||
function canonicalizeData (object) {
|
||||
if (typeof object === 'number' && isNaN(object)) {
|
||||
throw new Error('NaN is not allowed');
|
||||
}
|
||||
|
||||
if (typeof object === 'number' && !isFinite(object)) {
|
||||
throw new Error('Infinity is not allowed');
|
||||
}
|
||||
|
||||
if (object === null || typeof object !== 'object') {
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
|
||||
if (object.toJSON instanceof Function) {
|
||||
return serialize(object.toJSON());
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
const values = object.reduce((t, cv, ci) => {
|
||||
const comma = ci === 0 ? '' : ',';
|
||||
const value = cv === undefined || typeof cv === 'symbol' ? null : cv;
|
||||
return `${t}${comma}${serialize(value)}`;
|
||||
}, '');
|
||||
return `[${values}]`;
|
||||
}
|
||||
|
||||
const values = Object.keys(object).sort().reduce((t, cv) => {
|
||||
if (object[cv] === undefined ||
|
||||
typeof object[cv] === 'symbol') {
|
||||
return t;
|
||||
}
|
||||
const comma = t.length === 0 ? '' : ',';
|
||||
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`;
|
||||
}, '');
|
||||
return `{${values}}`;
|
||||
};
|
||||
|
||||
function encodeSection(data, shouldCanonicalize = false) {
|
||||
if (shouldCanonicalize) {
|
||||
return encodeBase64url(canonicalizeData(data));
|
||||
} else {
|
||||
return encodeBase64url(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function encodeBase64url(s) {
|
||||
return bytesToBase64url(u8a.fromString(s))
|
||||
}
|
||||
|
||||
function instanceOfEcdsaSignature(object) {
|
||||
return typeof object === 'object' && 'r' in object && 's' in object;
|
||||
}
|
||||
|
||||
function ES256KSignerAlg(recoverable) {
|
||||
return async function sign(payload, signer) {
|
||||
const signature = await signer(payload);
|
||||
if (instanceOfEcdsaSignature(signature)) {
|
||||
return toJose(signature, recoverable);
|
||||
} else {
|
||||
if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') {
|
||||
throw new Error(`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`);
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function leftpad(data, size = 64) {
|
||||
if (data.length === size) return data;
|
||||
return '0'.repeat(size - data.length) + data;
|
||||
}
|
||||
|
||||
|
||||
async function SimpleSigner(hexPrivateKey) {
|
||||
const signer = await ES256KSigner(hexToBytes(hexPrivateKey), true);
|
||||
return async (data) => {
|
||||
const signature = (await signer(data));
|
||||
return fromJose(signature);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function hexToBytes(s, minLength) {
|
||||
let input = s.startsWith('0x') ? s.substring(2) : s;
|
||||
|
||||
if (input.length % 2 !== 0) {
|
||||
input = `0${input}`;
|
||||
}
|
||||
|
||||
if (minLength) {
|
||||
const paddedLength = Math.max(input.length, minLength * 2);
|
||||
input = input.padStart(paddedLength, '00');
|
||||
}
|
||||
|
||||
return u8a.fromString(input.toLowerCase(), 'base16');
|
||||
}
|
||||
|
||||
|
||||
function ES256KSigner(privateKey, recoverable = false) {
|
||||
const privateKeyBytes = privateKey;
|
||||
if (privateKeyBytes.length !== 32) {
|
||||
throw new Error(`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`);
|
||||
}
|
||||
|
||||
return async function(data) {
|
||||
const privateKey = Secp256k1.uint256(privateKeyBytes, 16);
|
||||
const hash = await sha256(data);
|
||||
const digest = Secp256k1.uint256(hash, 16);
|
||||
console.error(privateKey, digest);
|
||||
const signature = Secp256k1.ecsign(privateKey, digest);
|
||||
const sigR = Secp256k1.uint256(signature.r,16);
|
||||
const sigS = Secp256k1.uint256(signature.s,16);
|
||||
return toJose({
|
||||
r: leftpad(signature.r.toString(16)),
|
||||
s: leftpad(signature.s.toString(16)),
|
||||
recoveryParam: signature.v,
|
||||
}, recoverable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toJose(signature, recoverable) {
|
||||
const { r, s, recoveryParam } = signature;
|
||||
const jose = new Uint8Array(recoverable ? 65 : 64);
|
||||
jose.set(u8a.fromString(r, 'base16'), 0);
|
||||
jose.set(u8a.fromString(s, 'base16'), 32);
|
||||
|
||||
if (recoverable) {
|
||||
if (typeof recoveryParam === 'undefined') {
|
||||
throw new Error('Signer did not return a recoveryParam');
|
||||
}
|
||||
jose[64] = recoveryParam;
|
||||
}
|
||||
return bytesToBase64url(jose);
|
||||
}
|
||||
|
||||
|
||||
function bytesToBase64url(b) {
|
||||
return u8a.toString(b, 'base64url');
|
||||
}
|
||||
|
||||
function base64ToBytes(s) {
|
||||
const inputBase64Url = s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
||||
return u8a.fromString(inputBase64Url, 'base64url')
|
||||
}
|
||||
|
||||
function bytesToHex(b) {
|
||||
return u8a.toString(b, 'base16')
|
||||
}
|
||||
|
||||
function fromJose(signature) {
|
||||
const signatureBytes = 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 };
|
||||
}
|
||||
|
||||
|
||||
function validateBase64(s) {
|
||||
if (
|
||||
!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(
|
||||
@@ -107,6 +438,15 @@ async function getNotificationCount() {
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
|
||||
|
||||
const msg = decoder.decode(decrypted);
|
||||
const identifier = JSON.parse(JSON.parse(msg));
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
headers["Authorization"] = "Bearer " + await accessToken(identifier);
|
||||
|
||||
result = decrypted;
|
||||
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user