Skip to content

Commit

Permalink
Merge pull-request #282
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkmin committed May 24, 2024
2 parents b52db56 + ccad348 commit b04de62
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 6 deletions.
6 changes: 5 additions & 1 deletion packages/sdk-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@
},
"dependencies": {
"@turnkey/api-key-stamper": "workspace:*",
"@turnkey/crypto": "workspace:*",
"@turnkey/encoding": "workspace:*",
"@turnkey/http": "workspace:*",
"@turnkey/iframe-stamper": "workspace:*",
"@turnkey/webauthn-stamper": "workspace:*",
"bs58check": "^3.0.1",
"buffer": "^6.0.3",
"cross-fetch": "^3.1.5",
"elliptic": "^6.5.5",
"cross-fetch": "^3.1.5"
"hpke-js": "^1.2.7"
},
"devDependencies": {
"@types/elliptic": "^6.4.18",
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk-browser/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ export interface SubOrganization {
organizationId: string;
organizationName: string;
}

export type EmbeddedAPIKey = {
authBundle: string;
publicKey: string;
};
44 changes: 42 additions & 2 deletions packages/sdk-browser/src/sdk-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import {
removeStorageValue,
setStorageValue,
} from "./storage";
import { generateRandomBuffer, base64UrlEncode } from "./utils";
import {
generateRandomBuffer,
base64UrlEncode,
createEmbeddedAPIKey,
} from "./utils";

export class TurnkeyBrowserSDK {
config: TurnkeySDKBrowserConfig;
Expand Down Expand Up @@ -141,7 +145,7 @@ export class TurnkeyBrowserSDK {
return data as TResponseType;
};

// Local
// Local Storage
getCurrentSubOrganization = async (): Promise<
SubOrganization | undefined
> => {
Expand All @@ -158,6 +162,10 @@ export class TurnkeyBrowserSDK {

return true;
};

getAuthBundle = async (): Promise<string | undefined> => {
return await getStorageValue(StorageKeys.AuthBundle);
};
}

export class TurnkeyBrowserClient extends TurnkeySDKClientBase {
Expand Down Expand Up @@ -246,6 +254,38 @@ export class TurnkeyPasskeyClient extends TurnkeyBrowserClient {
attestation: attestation,
};
};

// createPasskeySession creates a session authenticated by passkey, via an embedded API key,
// and stores + returns the resulting auth bundle that contains the encrypted API key.
createPasskeySession = async (
userId: string,
targetEmbeddedKey: string,
expirationSeconds?: string
): Promise<string> => {
const localStorageUser = await getStorageValue(StorageKeys.CurrentUser);
userId = userId ?? localStorageUser?.userId;

const { authBundle, publicKey } = await createEmbeddedAPIKey(
targetEmbeddedKey
);

// add API key to Turnkey User
await this.createApiKeys({
userId,
apiKeys: [
{
apiKeyName: `Session Key ${String(Date.now())}`,
publicKey,
expirationSeconds: expirationSeconds ?? "900", // default to 15 minutes
},
],
});

// store auth bundle in local storage
await setStorageValue(StorageKeys.AuthBundle, authBundle);

return authBundle;
};
}

export class TurnkeyIframeClient extends TurnkeyBrowserClient {
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk-browser/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import WindowWrapper from "./__polyfills__/window";

export enum StorageKeys {
CurrentUser = "@turnkey/current_user",
AuthBundle = "@turnkey/auth_bundle",
}

interface StorageValue {
[StorageKeys.CurrentUser]: User;
[StorageKeys.AuthBundle]: string;
}

enum StorageLocation {
Expand All @@ -17,6 +19,7 @@ enum StorageLocation {

const STORAGE_VALUE_LOCATIONS: Record<StorageKeys, StorageLocation> = {
[StorageKeys.CurrentUser]: StorageLocation.Local,
[StorageKeys.AuthBundle]: StorageLocation.Local,
};

const STORAGE_LOCATIONS = {
Expand Down
69 changes: 69 additions & 0 deletions packages/sdk-browser/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,73 @@
import { uint8ArrayFromHexString } from "@turnkey/encoding";
import {
generateP256KeyPair,
buildAdditionalAssociatedData,
compressRawPublicKey,
} from "@turnkey/crypto";

import { Buffer } from "buffer";
import bs58check from "bs58check";
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";

import type { EmbeddedAPIKey } from "./models";

// createEmbeddedAPIKey creates an embedded API key encrypted to a target key (typically embedded within an iframe).
// This returns a bundle that can be decrypted by that target key, as well as the public key of the newly created API key.
export const createEmbeddedAPIKey = async (
targetPublicKey: string
): Promise<EmbeddedAPIKey> => {
const TURNKEY_HPKE_INFO = new TextEncoder().encode("turnkey_hpke");

// 1: create new API key (to be encrypted to the targetPublicKey)
const p256key = generateP256KeyPair();

// 2: set up encryption
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes256Gcm,
});

// 3: import the targetPublicKey (i.e. passed in from the iframe)
const targetKeyBytes = uint8ArrayFromHexString(targetPublicKey);
const targetKey = await crypto.subtle.importKey(
"raw",
targetKeyBytes,
{
name: "ECDH",
namedCurve: "P-256",
},
true,
[]
);

// 4: sender encrypts a message to the target key
const sender = await suite.createSenderContext({
recipientPublicKey: targetKey,
info: TURNKEY_HPKE_INFO,
});
const ciphertext = await sender.seal(
uint8ArrayFromHexString(p256key.privateKey),
buildAdditionalAssociatedData(new Uint8Array(sender.enc), targetKeyBytes)
);
const ciphertextUint8Array = new Uint8Array(ciphertext);

// 5: assemble bundle
const encappedKey = new Uint8Array(sender.enc);
const compressedEncappedKey = compressRawPublicKey(encappedKey);
const result = new Uint8Array(
compressedEncappedKey.length + ciphertextUint8Array.length
);
result.set(compressedEncappedKey);
result.set(ciphertextUint8Array, compressedEncappedKey.length);

const base58encodedBundle = bs58check.encode(result);

return {
authBundle: base58encodedBundle,
publicKey: p256key.publicKey,
};
};

export const generateRandomBuffer = (): ArrayBuffer => {
const arr = new Uint8Array(32);
Expand Down
47 changes: 44 additions & 3 deletions packages/sdk-react/src/contexts/TurnkeyContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import {
TurnkeyIframeClient,
TurnkeyPasskeyClient,
TurnkeySDKBrowserConfig,
TurnkeyBrowserClient,
} from "@turnkey/sdk-browser";

export interface TurnkeyClientType {
turnkey: Turnkey | undefined;
authIframeClient: TurnkeyIframeClient | undefined;
passkeyClient: TurnkeyPasskeyClient | undefined;
getActiveClient: () => Promise<TurnkeyBrowserClient | undefined>;
}

export const TurnkeyContext = createContext<TurnkeyClientType>({
turnkey: undefined,
passkeyClient: undefined,
authIframeClient: undefined,
getActiveClient: async () => {
return undefined;
},
});

interface TurnkeyProviderProps {
Expand All @@ -36,18 +41,53 @@ export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
>(undefined);
const iframeInit = useRef<boolean>(false);

const TurnkeyIframeContainerId = "turnkey-default-iframe-container-id";
const TurnkeyAuthIframeContainerId = "turnkey-auth-iframe-container-id";
const TurnkeyAuthIframeElementId = "turnkey-auth-iframe-element-id";

const getActiveClient = async () => {
let currentClient: TurnkeyBrowserClient | undefined = passkeyClient;

try {
const authBundle = await turnkey?.getAuthBundle();

if (authBundle) {
const injected = await authIframeClient?.injectCredentialBundle(
authBundle
);
if (injected) {
const currentUser = await turnkey?.getCurrentUser();
await authIframeClient?.getWhoami({
organizationId:
currentUser?.organization.organizationId ??
turnkey?.config.defaultOrganizationId!,
});

currentClient = authIframeClient;
}
}
} catch (err: any) {
console.error("Failed to use iframe client", err);
console.log("Defaulting to passkey client");
}

return currentClient;
};

useEffect(() => {
(async () => {
if (!iframeInit.current) {
iframeInit.current = true;

const newTurnkey = new Turnkey(config);
setTurnkey(newTurnkey);
setPasskeyClient(newTurnkey.passkeyClient());

const newAuthIframeClient = await newTurnkey.iframeClient({
iframeContainer: document.getElementById(TurnkeyIframeContainerId),
iframeContainer: document.getElementById(
TurnkeyAuthIframeContainerId
),
iframeUrl: "https://auth.turnkey.com",
iframeElementId: TurnkeyAuthIframeElementId,
});
setAuthIframeClient(newAuthIframeClient);
}
Expand All @@ -60,12 +100,13 @@ export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
turnkey,
passkeyClient,
authIframeClient,
getActiveClient,
}}
>
{children}
<div
className=""
id={TurnkeyIframeContainerId}
id={TurnkeyAuthIframeContainerId}
style={{ display: "none" }}
/>
</TurnkeyContext.Provider>
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b04de62

Please sign in to comment.