From f6c70969e33c32b1eb421ec822a34eca1a91be62 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Nov 2024 20:28:46 -0800 Subject: [PATCH 1/4] release --- examples/typescript/package.json | 2 + examples/typescript/pnpm-lock.yaml | 35 +++++++++++++++ src/core/crypto/keyless.ts | 71 +++++++++++++++++++++++++----- 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/examples/typescript/package.json b/examples/typescript/package.json index 7b186df56..df25ba2b6 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -29,7 +29,9 @@ "dependencies": { "@noble/curves": "^1.4.0", "@types/readline-sync": "^1.4.8", + "axios": "^1.7.7", "dotenv": "^16.3.1", + "jwt-decode": "^4.0.0", "npm-run-all": "latest", "readline-sync": "^1.4.10", "superagent": "^8.1.2" diff --git a/examples/typescript/pnpm-lock.yaml b/examples/typescript/pnpm-lock.yaml index f9ef022f5..f1dc1ed84 100644 --- a/examples/typescript/pnpm-lock.yaml +++ b/examples/typescript/pnpm-lock.yaml @@ -14,9 +14,15 @@ dependencies: '@types/readline-sync': specifier: ^1.4.8 version: 1.4.8 + axios: + specifier: ^1.7.7 + version: 1.7.7 dotenv: specifier: ^16.3.1 version: 16.3.1 + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 npm-run-all: specifier: latest version: 4.1.5 @@ -155,6 +161,16 @@ packages: engines: {node: '>= 0.4'} dev: false + /axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: false @@ -357,6 +373,16 @@ packages: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} dev: false + /follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -613,6 +639,11 @@ packages: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} dev: false + /jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + dev: false + /load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -761,6 +792,10 @@ packages: engines: {node: '>=4'} dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /qs@6.11.2: resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} engines: {node: '>=0.6'} diff --git a/src/core/crypto/keyless.ts b/src/core/crypto/keyless.ts index 426f4c654..34c350928 100644 --- a/src/core/crypto/keyless.ts +++ b/src/core/crypto/keyless.ts @@ -44,6 +44,62 @@ export const MAX_EXTRA_FIELD_BYTES = 350; export const MAX_JWT_HEADER_B64_BYTES = 300; export const MAX_COMMITED_EPK_BYTES = 93; +function b64DecodeUnicode(str: string) { + return decodeURIComponent( + atob(str).replace(/(.)/g, (m, p) => { + let code = (p as string).charCodeAt(0).toString(16).toUpperCase(); + if (code.length < 2) { + code = `0${ code}`; + } + return `%${ code}`; + }), + ); +} + +function base64UrlDecode(str: string) { + let output = str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw new Error("base64 string is not of the correct length"); + } + + try { + return b64DecodeUnicode(output); + } catch (err) { + return atob(output); + } +} + +function getClaim(jwt: string, claim: string): string { + const parts = jwt.split("."); + const payload = parts[1]; + const payloadStr = base64UrlDecode(payload); + const claimIdx = payloadStr.indexOf(`"${claim}"`) + claim.length + 2; + let claimVal = ""; + let foundStart = false; + for (let i = claimIdx; i < payloadStr.length; i += 1) { + if (payloadStr[i] === "\"") { + if (foundStart) { + break; + } + foundStart = true; + continue; + } + if (foundStart) { + claimVal += payloadStr[i]; + } + } + return claimVal; +} + /** * Represents a Keyless Public Key used for authentication. * @@ -206,15 +262,9 @@ export class KeylessPublicKey extends AccountPublicKey { */ static fromJwtAndPepper(args: { jwt: string; pepper: HexInput; uidKey?: string }): KeylessPublicKey { const { jwt, pepper, uidKey = "sub" } = args; - const jwtPayload = jwtDecode(jwt); - if (typeof jwtPayload.iss !== "string") { - throw new Error("iss was not found"); - } - if (typeof jwtPayload.aud !== "string") { - throw new Error("aud was not found or an array of values"); - } - const uidVal = jwtPayload[uidKey]; - return KeylessPublicKey.create({ iss: jwtPayload.iss, uidKey, uidVal, aud: jwtPayload.aud, pepper }); + + const { iss, aud, uidVal } = getIssAudAndUidVal({ jwt, uidKey }); + return KeylessPublicKey.create({ iss, uidKey, uidVal, aud, pepper }); } /** @@ -798,8 +848,7 @@ export function getIssAudAndUidVal(args: { jwt: string; uidKey?: string }): { details: "JWT is missing 'aud' in the payload or 'aud' is an array of values.", }); } - const uidVal = jwtPayload[uidKey]; - return { iss: jwtPayload.iss, aud: jwtPayload.aud, uidVal }; + return { iss: getClaim(jwt, "iss"), aud: getClaim(jwt, "aud"), uidVal: getClaim(jwt, uidKey) }; } /** From 5de95f6dcaa3ccbcdbf3f1c5796a49d16959ba36 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Nov 2024 20:28:58 -0800 Subject: [PATCH 2/4] package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c40e131b..28d030442 100644 --- a/package.json +++ b/package.json @@ -96,5 +96,5 @@ "typedoc-plugin-missing-exports": "^3.0.0", "typescript": "^5.6.2" }, - "version": "1.33.0" + "version": "1.33.0-zeta.0" } \ No newline at end of file From 242f225425c16d14a7cf6c9168db6c44af742cd8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 13 Dec 2024 03:12:32 +0900 Subject: [PATCH 3/4] Return a multikey for cognito --- src/core/crypto/federatedKeyless.ts | 16 +++++- src/core/crypto/keyless.ts | 79 +++++++++-------------------- src/core/crypto/utils.ts | 57 +++++++++++++++++++++ src/internal/keyless.ts | 48 ++++++++++++++++-- src/utils/const.ts | 2 + 5 files changed, 141 insertions(+), 61 deletions(-) diff --git a/src/core/crypto/federatedKeyless.ts b/src/core/crypto/federatedKeyless.ts index 0a2fec22e..f3cea0cf4 100644 --- a/src/core/crypto/federatedKeyless.ts +++ b/src/core/crypto/federatedKeyless.ts @@ -6,7 +6,7 @@ import { Deserializer, Serializer } from "../../bcs"; import { HexInput, AnyPublicKeyVariant, SigningScheme } from "../../types"; import { AuthenticationKey } from "../authenticationKey"; import { AccountAddress, AccountAddressInput } from "../accountAddress"; -import { KeylessPublicKey, KeylessSignature } from "./keyless"; +import { getIssAudAndUidVal, KeylessPublicKey, KeylessSignature } from "./keyless"; /** * Represents the FederatedKeylessPublicKey public key @@ -104,6 +104,20 @@ export class FederatedKeylessPublicKey extends AccountPublicKey { return new FederatedKeylessPublicKey(args.jwkAddress, KeylessPublicKey.fromJwtAndPepper(args)); } + static fromJwtAndPepperWithoutUnescaping(args: { + jwt: string; + pepper: HexInput; + jwkAddress: AccountAddressInput; + uidKey?: string; + }): FederatedKeylessPublicKey { + const { jwt, pepper, uidKey = "sub" } = args; + const { iss, aud, uidVal } = getIssAudAndUidVal({ jwt, uidKey }); + return new FederatedKeylessPublicKey( + args.jwkAddress, + KeylessPublicKey.create({ iss, uidKey, uidVal, aud, pepper }), + ); + } + static isInstance(publicKey: PublicKey) { return ( "jwkAddress" in publicKey && diff --git a/src/core/crypto/keyless.ts b/src/core/crypto/keyless.ts index 34c350928..da067da14 100644 --- a/src/core/crypto/keyless.ts +++ b/src/core/crypto/keyless.ts @@ -34,6 +34,7 @@ import { memoizeAsync } from "../../utils/memoize"; import { AccountAddress, AccountAddressInput } from "../accountAddress"; import { getErrorMessage } from "../../utils"; import { KeylessError, KeylessErrorType } from "../../errors"; +import { getClaimWithoutUnescaping } from "./utils"; export const EPK_HORIZON_SECS = 10000000; export const MAX_AUD_VAL_BYTES = 120; @@ -44,62 +45,6 @@ export const MAX_EXTRA_FIELD_BYTES = 350; export const MAX_JWT_HEADER_B64_BYTES = 300; export const MAX_COMMITED_EPK_BYTES = 93; -function b64DecodeUnicode(str: string) { - return decodeURIComponent( - atob(str).replace(/(.)/g, (m, p) => { - let code = (p as string).charCodeAt(0).toString(16).toUpperCase(); - if (code.length < 2) { - code = `0${ code}`; - } - return `%${ code}`; - }), - ); -} - -function base64UrlDecode(str: string) { - let output = str.replace(/-/g, "+").replace(/_/g, "/"); - switch (output.length % 4) { - case 0: - break; - case 2: - output += "=="; - break; - case 3: - output += "="; - break; - default: - throw new Error("base64 string is not of the correct length"); - } - - try { - return b64DecodeUnicode(output); - } catch (err) { - return atob(output); - } -} - -function getClaim(jwt: string, claim: string): string { - const parts = jwt.split("."); - const payload = parts[1]; - const payloadStr = base64UrlDecode(payload); - const claimIdx = payloadStr.indexOf(`"${claim}"`) + claim.length + 2; - let claimVal = ""; - let foundStart = false; - for (let i = claimIdx; i < payloadStr.length; i += 1) { - if (payloadStr[i] === "\"") { - if (foundStart) { - break; - } - foundStart = true; - continue; - } - if (foundStart) { - claimVal += payloadStr[i]; - } - } - return claimVal; -} - /** * Represents a Keyless Public Key used for authentication. * @@ -851,6 +796,28 @@ export function getIssAudAndUidVal(args: { jwt: string; uidKey?: string }): { return { iss: getClaim(jwt, "iss"), aud: getClaim(jwt, "aud"), uidVal: getClaim(jwt, uidKey) }; } +/** + * Parses a JWT and returns the 'iss', 'aud', and 'uid' values without unescaping the values. + * + * @param args - The arguments for parsing the JWT. + * @param args.jwt - The JWT to parse. + * @param args.uidKey - The key to use for the 'uid' value; defaults to 'sub'. + * @returns The 'iss', 'aud', and 'uid' values from the JWT. + */ +export function getIssAudAndUidValWithoutUnescaping(args: { jwt: string; uidKey?: string }): { + iss: string; + aud: string; + uidVal: string; +} { + const { jwt, uidKey = "sub" } = args; + getIssAudAndUidVal(args); + return { + iss: getClaimWithoutUnescaping(jwt, "iss"), + aud: getClaimWithoutUnescaping(jwt, "aud"), + uidVal: getClaimWithoutUnescaping(jwt, uidKey), + }; +} + /** * Retrieves the KeylessConfiguration set on chain. * diff --git a/src/core/crypto/utils.ts b/src/core/crypto/utils.ts index f75ef679b..e939b0a32 100644 --- a/src/core/crypto/utils.ts +++ b/src/core/crypto/utils.ts @@ -22,3 +22,60 @@ export const convertSigningMessage = (message: HexInput): HexInput => { // message is a Uint8Array return message; }; + +function b64DecodeUnicode(str: string) { + return decodeURIComponent( + atob(str).replace(/(.)/g, (m, p) => { + let code = (p as string).charCodeAt(0).toString(16).toUpperCase(); + if (code.length < 2) { + code = `0${code}`; + } + return `%${code}`; + }), + ); +} + +function base64UrlDecode(str: string) { + let output = str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw new Error("base64 string is not of the correct length"); + } + + try { + return b64DecodeUnicode(output); + } catch (err) { + return atob(output); + } +} + +export function getClaimWithoutUnescaping(jwt: string, claim: string): string { + const parts = jwt.split("."); + const payload = parts[1]; + const payloadStr = base64UrlDecode(payload); + const claimIdx = payloadStr.indexOf(`"${claim}"`) + claim.length + 2; + let claimVal = ""; + let foundStart = false; + for (let i = claimIdx; i < payloadStr.length; i += 1) { + if (payloadStr[i] === '"') { + if (foundStart) { + break; + } + foundStart = true; + // eslint-disable-next-line no-continue + continue; + } + if (foundStart) { + claimVal += payloadStr[i]; + } + } + return claimVal; +} diff --git a/src/internal/keyless.ts b/src/internal/keyless.ts index d02545dbc..9870eb208 100644 --- a/src/internal/keyless.ts +++ b/src/internal/keyless.ts @@ -17,12 +17,21 @@ import { Hex, KeylessPublicKey, MoveJWK, + MultiKey, ZeroKnowledgeSig, ZkProof, + getIssAudAndUidVal, getKeylessConfig, } from "../core"; import { HexInput, ZkpVariant } from "../types"; -import { Account, EphemeralKeyPair, KeylessAccount, ProofFetchCallback } from "../account"; +import { + Account, + EphemeralKeyPair, + KeylessAccount, + KeylessSigner, + MultiKeyAccount, + ProofFetchCallback, +} from "../account"; import { PepperFetchRequest, PepperFetchResponse, ProverRequest, ProverResponse } from "../types/keyless"; import { lookupOriginalAccountAddress } from "./account"; import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless"; @@ -31,7 +40,7 @@ import { MoveVector } from "../bcs"; import { generateTransaction } from "./transactionSubmission"; import { InputGenerateTransactionOptions, SimpleTransaction } from "../transactions"; import { KeylessError, KeylessErrorType } from "../errors"; -import { FIREBASE_AUTH_ISS_PATTERN } from "../utils/const"; +import { COGNITO_ISS_PATTERN, FIREBASE_AUTH_ISS_PATTERN } from "../utils/const"; /** * Retrieves a pepper value based on the provided configuration and authentication details. @@ -172,7 +181,7 @@ export async function deriveKeylessAccount(args: { uidKey?: string; pepper?: HexInput; proofFetchCallback?: ProofFetchCallback; -}): Promise; +}): Promise; export async function deriveKeylessAccount(args: { aptosConfig: AptosConfig; @@ -182,7 +191,7 @@ export async function deriveKeylessAccount(args: { uidKey?: string; pepper?: HexInput; proofFetchCallback?: ProofFetchCallback; -}): Promise { +}): Promise { const { aptosConfig, jwt, jwkAddress, uidKey, proofFetchCallback, pepper = await getPepper(args) } = args; const { verificationKey, maxExpHorizonSecs } = await getKeylessConfig({ aptosConfig }); @@ -196,6 +205,32 @@ export async function deriveKeylessAccount(args: { // Look up the original address to handle key rotations and then instantiate the account. if (jwkAddress !== undefined) { + if (isCognito(jwt)) { + const multiKey = new MultiKey({ + publicKeys: [ + FederatedKeylessPublicKey.fromJwtAndPepperWithoutUnescaping({ jwt, pepper, jwkAddress, uidKey }), + FederatedKeylessPublicKey.fromJwtAndPepper({ jwt, pepper, jwkAddress, uidKey }), + ], + signaturesRequired: 1, + }); + const address = await lookupOriginalAccountAddress({ + aptosConfig, + authenticationKey: multiKey.authKey().derivedAddress(), + }); + const signer = FederatedKeylessAccount.create({ + ...args, + address, + proof, + pepper, + proofFetchCallback, + jwkAddress, + verificationKey, + }); + return new MultiKeyAccount({ + multiKey, + signers: [signer], + }); + } const publicKey = FederatedKeylessPublicKey.fromJwtAndPepper({ jwt, pepper, jwkAddress, uidKey }); const address = await lookupOriginalAccountAddress({ aptosConfig, @@ -221,6 +256,11 @@ export async function deriveKeylessAccount(args: { return KeylessAccount.create({ ...args, address, proof, pepper, proofFetchCallback, verificationKey }); } +function isCognito(jwt: string): boolean { + const { iss } = getIssAudAndUidVal({ jwt }); + return COGNITO_ISS_PATTERN.test(iss); +} + export interface JWKS { keys: MoveJWK[]; } diff --git a/src/utils/const.ts b/src/utils/const.ts index d1641d695..d773dd6a5 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -72,3 +72,5 @@ export enum ProcessorType { * where project-id can contain letters, numbers, hyphens, and underscores */ export const FIREBASE_AUTH_ISS_PATTERN = /^https:\/\/securetoken\.google\.com\/[a-zA-Z0-9-_]+$/; + +export const COGNITO_ISS_PATTERN = /^https:\/\/cognito-idp\.[a-zA-Z0-9-_]+\.amazonaws\.com\/[a-zA-Z0-9-_]+$/; From b58cc1e3f5e27572e3980737fed04070c62b7179 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 13 Dec 2024 03:21:18 +0900 Subject: [PATCH 4/4] revert --- src/core/crypto/keyless.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/crypto/keyless.ts b/src/core/crypto/keyless.ts index ef35e9fc1..91f80b4eb 100644 --- a/src/core/crypto/keyless.ts +++ b/src/core/crypto/keyless.ts @@ -923,7 +923,8 @@ export function getIssAudAndUidVal(args: { jwt: string; uidKey?: string }): { details: "JWT is missing 'aud' in the payload or 'aud' is an array of values.", }); } - return { iss: getClaim(jwt, "iss"), aud: getClaim(jwt, "aud"), uidVal: getClaim(jwt, uidKey) }; + const uidVal = jwtPayload[uidKey]; + return { iss: jwtPayload.iss, aud: jwtPayload.aud, uidVal }; } /**