UNPKG

@stacks/profile

Version:

Library for Stacks profiles

162 lines (141 loc) 4.93 kB
import { decodeToken, SECP256K1Client, TokenSigner, TokenVerifier } from 'jsontokens'; import { TokenInterface } from 'jsontokens/lib/decode'; import { nextYear, makeUUID4 } from '@stacks/common'; import { getAddressFromPublicKey } from '@stacks/transactions'; /** * Signs a profile token * @param {Object} profile - the JSON of the profile to be signed * @param {String} privateKey - the signing private key * @param {Object} subject - the entity that the information is about * @param {Object} issuer - the entity that is issuing the token * @param {String} signingAlgorithm - the signing algorithm to use * @param {Date} issuedAt - the time of issuance of the token * @param {Date} expiresAt - the time of expiration of the token * @returns {Object} - the signed profile token * */ export function signProfileToken( profile: any, privateKey: string, subject?: any, issuer?: any, signingAlgorithm = 'ES256K', issuedAt = new Date(), expiresAt = nextYear() ): string { if (signingAlgorithm !== 'ES256K') { throw new Error('Signing algorithm not supported'); } const publicKey = SECP256K1Client.derivePublicKey(privateKey); if (!subject) { subject = { publicKey }; } if (!issuer) { issuer = { publicKey }; } const tokenSigner = new TokenSigner(signingAlgorithm, privateKey); const payload = { jti: makeUUID4(), iat: issuedAt.toISOString(), exp: expiresAt.toISOString(), subject, issuer, claim: profile, }; return tokenSigner.sign(payload); } /** * Wraps a token for a profile token file * @param {String} token - the token to be wrapped * @returns {Object} - including `token` and `decodedToken` */ export function wrapProfileToken(token: string) { return { token, decodedToken: decodeToken(token), }; } /** * Verifies a profile token * @param {String} token - the token to be verified * @param {String} publicKeyOrAddress - the public key or address of the * keypair that is thought to have signed the token * @returns {Object} - the verified, decoded profile token * @throws {Error} - throws an error if token verification fails */ export function verifyProfileToken(token: string, publicKeyOrAddress: string): TokenInterface { const decodedToken = decodeToken(token); const payload = decodedToken.payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } // Inspect and verify the subject if (payload.hasOwnProperty('subject') && payload.subject) { if (!payload.subject.hasOwnProperty('publicKey')) { throw new Error("Token doesn't have a subject public key"); } } else { throw new Error("Token doesn't have a subject"); } // Inspect and verify the issuer if (payload.hasOwnProperty('issuer') && payload.issuer) { if (!payload.issuer.hasOwnProperty('publicKey')) { throw new Error("Token doesn't have an issuer public key"); } } else { throw new Error("Token doesn't have an issuer"); } // Inspect and verify the claim if (!payload.hasOwnProperty('claim')) { throw new Error("Token doesn't have a claim"); } const issuerPublicKey = (payload.issuer as Record<string, string>).publicKey; const address = getAddressFromPublicKey(issuerPublicKey); if (publicKeyOrAddress === issuerPublicKey) { // pass } else if (publicKeyOrAddress === address) { // pass } else { throw new Error('Token issuer public key does not match the verifying value'); } const tokenVerifier = new TokenVerifier(decodedToken.header.alg as string, issuerPublicKey); if (!tokenVerifier) { throw new Error('Invalid token verifier'); } const tokenVerified = tokenVerifier.verify(token); if (!tokenVerified) { throw new Error('Token verification failed'); } return decodedToken; } /** * Extracts a profile from an encoded token and optionally verifies it, * if `publicKeyOrAddress` is provided. * @param {String} token - the token to be extracted * @param {String} publicKeyOrAddress - the public key or address of the * keypair that is thought to have signed the token * @returns {Object} - the profile extracted from the encoded token * @throws {Error} - if the token isn't signed by the provided `publicKeyOrAddress` */ export function extractProfile( token: string, publicKeyOrAddress: string | null = null ): Record<string, any> { let decodedToken; if (publicKeyOrAddress) { decodedToken = verifyProfileToken(token, publicKeyOrAddress); } else { decodedToken = decodeToken(token); } let profile = {}; if (decodedToken.hasOwnProperty('payload')) { const payload = decodedToken.payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } if (payload.hasOwnProperty('claim')) { profile = payload.claim as Record<string, any>; } } return profile; }