Skip to content

Commit

Permalink
smartpass (#3362)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Nov 8, 2024
1 parent d77b89f commit 4c25d1e
Show file tree
Hide file tree
Showing 33 changed files with 539 additions and 114 deletions.
5 changes: 5 additions & 0 deletions packages/entrykit/mprocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ procs:
shell: pnpm start
# explorer:
# shell: pnpm explorer
id:
cwd: ../id
shell: pnpm vite --port 5155
tunnel:
shell: cloudflared tunnel run
5 changes: 3 additions & 2 deletions packages/entrykit/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@latticexyz/entrykit",
"version": "2.2.8",
"description": "User onboarding flows for MUD projects",
"version": "0.0.0",
"description": "User onboarding flows for MUD apps",
"repository": {
"type": "git",
"url": "https://github.com/latticexyz/mud.git",
Expand Down Expand Up @@ -44,6 +44,7 @@
"@latticexyz/common": "workspace:*",
"@latticexyz/config": "workspace:*",
"@latticexyz/explorer": "workspace:*",
"@latticexyz/id": "workspace:*",
"@latticexyz/paymaster": "workspace:*",
"@latticexyz/protocol-parser": "workspace:*",
"@latticexyz/store": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/entrykit/playground/anvil-state.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/entrykit/playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ export default defineConfig({
},
},
},
server: {
headers: {
"Permissions-Policy": "publickey-credentials-get=*, publickey-credentials-create=*",
},
},
});
6 changes: 3 additions & 3 deletions packages/entrykit/playground/wagmiConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chain, webSocket } from "viem";
import { Chain, http } from "viem";
import { anvil } from "viem/chains";
import { createWagmiConfig } from "../src/createWagmiConfig";
import { chainId } from "./common";
Expand All @@ -23,7 +23,7 @@ const chains = [
] as const satisfies Chain[];

const transports = {
[anvil.id]: webSocket(),
[anvil.id]: http(),
} as const;

export const wagmiConfig = createWagmiConfig({
Expand All @@ -33,6 +33,6 @@ export const wagmiConfig = createWagmiConfig({
chains,
transports,
pollingInterval: {
[anvil.id]: 2000,
[anvil.id]: 500,
},
});
30 changes: 17 additions & 13 deletions packages/entrykit/src/passkey/createPasskey.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { createCredential } from "webauthn-p256";
import { cache } from "./cache";
import { P256Credential } from "viem/account-abstraction";
import { createBridge, PasskeyCredential } from "@latticexyz/id/internal";

export async function createPasskey(): Promise<P256Credential> {
const credential = await createCredential({ name: "MUD Account" });
console.log("created passkey", credential);
export async function createPasskey(): Promise<PasskeyCredential> {
const bridge = await createBridge({ message: "Creating account…" });
try {
const credential = await bridge.request("create");
console.log("created passkey", credential);

cache.setState((state) => ({
activeCredential: credential.id,
publicKeys: {
...state.publicKeys,
[credential.id]: credential.publicKey,
},
}));
cache.setState((state) => ({
activeCredential: credential.credentialId,
publicKeys: {
...state.publicKeys,
[credential.credentialId]: credential.publicKey,
},
}));

return credential;
return credential;
} finally {
bridge.close();
}
}
19 changes: 19 additions & 0 deletions packages/entrykit/src/passkey/findPublicKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getCandidatePublicKeys } from "./getCandidatePublicKeys";
import { SignatureAndMessage } from "./common";
import { Hex } from "viem";

export function findPublicKey([input1, input2]: [SignatureAndMessage, SignatureAndMessage]): Hex | undefined {
// Return the candidate public key that appears twice
return firstDuplicate([...getCandidatePublicKeys(input1), ...getCandidatePublicKeys(input2)]);
}

function firstDuplicate<T>(arr: T[]): T | undefined {
const seen = new Set<T>();
for (const s of arr) {
if (seen.has(s)) {
return s;
}
seen.add(s);
}
return undefined;
}
1 change: 1 addition & 0 deletions packages/entrykit/src/passkey/getMessageHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export async function getMessageHash(
const match = clientDataJSON.slice(Number(challengeIndex)).match(/^"challenge":"(.*?)"/);
if (!match) throw new Error("Invalid clientDataJSON");

// TODO: switch to ox, then maybe don't need async/await here
const clientDataJSONHash = new Uint8Array(await crypto.subtle.digest("SHA-256", utf8ToBytes(clientDataJSON)));
const messageHash = new Uint8Array(
await crypto.subtle.digest("SHA-256", concatBytes(hexToBytes(authenticatorData), clientDataJSONHash)),
Expand Down
8 changes: 4 additions & 4 deletions packages/entrykit/src/passkey/passkeyConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ export function passkeyConnector({ chainId }: PasskeyConnectorOptions): CreatePa
// supportsSimulation: true,

async createPasskey() {
const { id } = await createPasskey();
const account = await getAccount(client, id);
const { credentialId } = await createPasskey();
const account = await getAccount(client, credentialId);
this.onAccountsChanged([account.address]);
this.onConnect?.({ chainId: numberToHex(chainId) });
},
async reusePasskey() {
const { id } = await reusePasskey();
const account = await getAccount(client, id);
const { credentialId } = await reusePasskey();
const account = await getAccount(client, credentialId);
this.onAccountsChanged([account.address]);
this.onConnect?.({ chainId: numberToHex(chainId) });
},
Expand Down
37 changes: 0 additions & 37 deletions packages/entrykit/src/passkey/recoverPasskeyPublicKey.ts

This file was deleted.

91 changes: 48 additions & 43 deletions packages/entrykit/src/passkey/reusePasskey.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
import { bytesToHex, hashMessage } from "viem";
import { sign } from "webauthn-p256";
import { cache } from "./cache";
import { getMessageHash } from "./getMessageHash";
import { recoverPasskeyPublicKey } from "./recoverPasskeyPublicKey";
import { P256Credential } from "viem/account-abstraction";

export async function reusePasskey(): Promise<P256Credential> {
const randomChallenge = bytesToHex(crypto.getRandomValues(new Uint8Array(256)));
const messageHash = hashMessage(randomChallenge);
const { signature, webauthn, raw: credential } = await sign({ hash: messageHash });

const publicKey = await (async () => {
const publicKey = cache.getState().publicKeys[credential.id];
if (publicKey) return publicKey;

// TODO: look up account/public key by credential ID once we store it onchain

const webauthnHash = await getMessageHash(webauthn);
const passkey = await recoverPasskeyPublicKey({
credentialId: credential.id,
messageHash: webauthnHash,
signatureHex: signature,
});
if (!passkey) {
throw new Error("recovery failed");
}
if (passkey.credential.id !== credential.id) {
throw new Error("wrong credential");
}

cache.setState((state) => ({
publicKeys: {
...state.publicKeys,
[credential.id]: passkey.publicKey,
},
import { findPublicKey } from "./findPublicKey";
import { PasskeyCredential, createBridge } from "@latticexyz/id/internal";

export async function reusePasskey(): Promise<PasskeyCredential> {
const bridge = await createBridge({ message: "Signing in…" });
try {
const challenge = hashMessage(bytesToHex(crypto.getRandomValues(new Uint8Array(256))));
const { credentialId, signature, metadata } = await bridge.request("sign", { challenge });

const publicKey = await (async () => {
const cachedPublicKey = cache.getState().publicKeys[credentialId];
if (cachedPublicKey) return cachedPublicKey;

// TODO: look up account/public key by credential ID once we store it onchain

const messageHash = await getMessageHash(metadata);
const challenge2 = hashMessage(signature);
const signature2 = await bridge.request("sign", { credentialId, challenge: challenge2 });
if (signature2.credentialId !== credentialId) {
throw new Error("wrong credential");
}

const publicKey = findPublicKey([
{ messageHash, signatureHex: signature },
{ messageHash: await getMessageHash(signature2.metadata), signatureHex: signature2.signature },
]);
if (!publicKey) {
throw new Error("recovery failed");
}

cache.setState((state) => ({
publicKeys: {
...state.publicKeys,
[credentialId]: publicKey,
},
}));

return publicKey;
})();

console.log("recovered passkey", credentialId, publicKey);

cache.setState(() => ({
activeCredential: credentialId,
}));

return passkey.publicKey;
})();

console.log("recovered passkey", credential.id, publicKey);

cache.setState(() => ({
activeCredential: credential.id,
}));

return { id: credential.id, publicKey, raw: credential };
return { credentialId, publicKey };
} finally {
bridge.close();
}
}
3 changes: 3 additions & 0 deletions packages/id/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["../../.eslintrc"]
}
1 change: 1 addition & 0 deletions packages/id/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO
25 changes: 25 additions & 0 deletions packages/id/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MUD ID</title>
<style>
body {
background: black;
color: white;
font-family: sans-serif;
}
#message {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
</style>
</head>
<body>
<div id="message"></div>
<script type="module" src="./src/index.ts"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/id/mprocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
procs:
client:
shell: pnpm vite
46 changes: 46 additions & 0 deletions packages/id/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@latticexyz/id",
"version": "0.0.0",
"description": "Simple MUD accounts with passkeys",
"repository": {
"type": "git",
"url": "https://github.com/latticexyz/mud.git",
"directory": "packages/id"
},
"license": "MIT",
"type": "module",
"exports": {
"./internal": "./dist/internal.js"
},
"typesVersions": {
"*": {
"internal": [
"./dist/internal.d.ts"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"clean": "shx rm -rf dist",
"dev": "tsup --watch",
"test": "tsc --noEmit && vitest --run",
"test:ci": "pnpm run test"
},
"dependencies": {
"@ark/util": "0.2.2",
"debug": "^4.3.4",
"ox": "0.1.0"
},
"devDependencies": {
"@types/debug": "^4.1.7",
"mprocs": "^0.7.1",
"tsup": "^6.7.0",
"vite": "^5.4.1"
},
"publishConfig": {
"access": "public"
}
}
Loading

0 comments on commit 4c25d1e

Please sign in to comment.