Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-cows-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

✨ use credential salt for derivation
5 changes: 5 additions & 0 deletions .changeset/neat-eggs-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/server": patch
---

✨ add credential salt responses
5 changes: 5 additions & 0 deletions .changeset/quick-koalas-thread.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/common": patch
---

✨ add credential salt contract
6 changes: 3 additions & 3 deletions common/accountInit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { encodeFunctionData, hexToBigInt, type Hash } from "viem";
import { encodeFunctionData, hexToBigInt, zeroAddress, type Hash } from "viem";

import { exaAccountFactoryAbi } from "./generated/chain";

export default function accountInit({ x, y }: { x: Hash; y: Hash }) {
export default function accountInit({ salt = zeroAddress, x, y }: { salt?: string; x: Hash; y: Hash }) {
Comment thread
aguxez marked this conversation as resolved.
return encodeFunctionData({
abi: exaAccountFactoryAbi,
functionName: "createAccount",
args: [0n, [{ x: hexToBigInt(x), y: hexToBigInt(y) }]],
args: [BigInt(salt), [{ x: hexToBigInt(x), y: hexToBigInt(y) }]],
});
}
17 changes: 14 additions & 3 deletions common/deriveAddress.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { parse } from "valibot";
import { encodeAbiParameters, encodePacked, keccak256, slice, type Hash, type Address as ViemAddress } from "viem";
import {
encodeAbiParameters,
encodePacked,
keccak256,
slice,
zeroAddress,
type Hash,
type Address as ViemAddress,
} from "viem";

import { Address } from "./validation";

Expand All @@ -15,7 +23,10 @@ const initCodeHashERC1967 = keccak256(
),
);

export default function deriveAddress(factory: ViemAddress, { x, y }: { x: Hash; y: Hash }) {
export default function deriveAddress(
factory: ViemAddress,
{ salt = zeroAddress, x, y }: { salt?: string; x: Hash; y: Hash },
) {
return parse(
Address,
slice(
Expand All @@ -29,7 +40,7 @@ export default function deriveAddress(factory: ViemAddress, { x, y }: { x: Hash;
encodeAbiParameters(
[{ type: "uint256" }, { type: "bytes" }],
[
0n,
BigInt(salt),
encodeAbiParameters(
[
{
Expand Down
15 changes: 14 additions & 1 deletion common/validation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { brand, check, custom, object, pipe, regex, string, title, transform, type InferOutput } from "valibot";
import {
brand,
check,
custom,
object,
optional,
pipe,
regex,
string,
title,
transform,
type InferOutput,
} from "valibot";
import {
checksumAddress,
isAddress,
Expand Down Expand Up @@ -28,6 +40,7 @@ export const Credential = pipe(
factory: pipe(Address, title("Account factory address")),
x: pipe(Hash, title("Credential public key x coordinate")),
y: pipe(Hash, title("Credential public key y coordinate")),
salt: optional(pipe(Address, title("Credential salt"))),
}),
title("WebAuthn passkey metadata"),
);
Expand Down
5 changes: 3 additions & 2 deletions server/api/auth/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se
if (!sessionId) return c.json({ code: "bad session" }, 400);
const [credential, challenge] = await Promise.all([
database.query.credentials.findFirst({
columns: { publicKey: true, account: true, factory: true, transports: true },
columns: { publicKey: true, account: true, factory: true, salt: true, transports: true },
where: eq(credentials.id, assertion.id),
}),
redis.getdel(sessionId),
Expand All @@ -393,7 +393,7 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se
source: c.req.header("Client-Fid"),
ip: headers?.["do-connecting-ip"],
});
const account = deriveAddress(result.factory, { x: result.x, y: result.y });
const account = deriveAddress(result.factory, { x: result.x, y: result.y, salt: result.salt });
const intercomToken = await intercom(account, result.auth);
return c.json(
{
Expand Down Expand Up @@ -468,6 +468,7 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se
credentialId: assertion.id,
factory: parse(Address, credential.factory),
...decodePublicKey(credential.publicKey),
salt: parse(Address, credential.salt),
auth: expires.getTime(),
expires: expires.getTime(),
intercomToken,
Expand Down
2 changes: 1 addition & 1 deletion server/api/auth/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ export default function route({
source: headers?.["Client-Fid"],
ip: headers?.["do-connecting-ip"],
});
const account = deriveAddress(result.factory, { x: result.x, y: result.y });
const account = deriveAddress(result.factory, { x: result.x, y: result.y, salt: result.salt });
const intercomToken = await intercom(account, new Date(Date.now() + AUTH_EXPIRY));
return c.json(
{
Expand Down
11 changes: 10 additions & 1 deletion server/api/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,15 @@ async function encryptPIN(pin: string) {
return mutex
.runExclusive(async () => {
const credential = await database.query.credentials.findFirst({
columns: { account: true, factory: true, pandaId: true, publicKey: true, source: true, transports: true },
columns: {
account: true,
factory: true,
pandaId: true,
publicKey: true,
salt: true,
source: true,
transports: true,
},
where: eq(credentials.id, credentialId),
with: {
cards: {
Expand Down Expand Up @@ -948,6 +956,7 @@ async function encryptPIN(pin: string) {
},
assertion: patch.assertion,
factory: credential.factory,
salt: parse(Address, credential.salt),
statement,
});
} catch (error) {
Expand Down
7 changes: 4 additions & 3 deletions server/api/kyc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function route({

const { credentialId } = c.req.valid("cookie");
const credential = await database.query.credentials.findFirst({
columns: { id: true, account: true, pandaId: true, factory: true, publicKey: true },
columns: { id: true, account: true, pandaId: true, factory: true, publicKey: true, salt: true },
where: eq(credentials.id, credentialId),
});
if (!credential) return c.json({ code: "no credential", legacy: "no credential" }, 500);
Expand Down Expand Up @@ -152,7 +152,7 @@ export default function route({
return c.json({ code: "ok", legacy: "ok" }, 200);
}

if (await isLegacy(credentialId, account, credential.factory, credential.publicKey, persona)) {
if (await isLegacy(credentialId, account, credential.factory, credential.publicKey, credential.salt, persona)) {
return c.json({ code: "legacy kyc", legacy: "legacy kyc" }, 200);
}

Expand Down Expand Up @@ -710,6 +710,7 @@ async function isLegacy(
account: Address,
factory: string,
publicKey: Uint8Array<ArrayBuffer>,
salt: string,
persona: ReturnType<typeof createPersona>,
): Promise<boolean> {
if (factory === exaAccountFactoryAddress) return false;
Expand All @@ -719,7 +720,7 @@ async function isLegacy(
functionName: "getInstalledPlugins",
abi: upgradeableModularAccountAbi,
factory: getAddress(factory),
factoryData: accountInit(decodePublicKey(publicKey)),
factoryData: accountInit({ ...decodePublicKey(publicKey), salt }),
});
if (installedPlugin.length === 0) return false;
if (installedPlugin.includes(exaPluginAddress)) return false;
Expand Down
3 changes: 2 additions & 1 deletion server/api/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ export default function route({ auth, database }: { auth: Auth; database: NodePg
const { credentialId } = c.req.valid("cookie");
const credential = await database.query.credentials.findFirst({
where: eq(credentials.id, credentialId),
columns: { publicKey: true, account: true, factory: true },
columns: { publicKey: true, account: true, factory: true, salt: true },
});
if (!credential) return c.json({ code: "no credential", legacy: "no credential" }, 500);
setUser({ id: parse(Address, credential.account) });
return c.json(
{
credentialId,
factory: parse(Address, credential.factory),
salt: parse(Address, credential.salt),
...decodePublicKey(credential.publicKey),
} satisfies InferOutput<typeof Credential>,
200,
Expand Down
11 changes: 9 additions & 2 deletions server/database/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { relations } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
bigint,
boolean,
char,
check,
customType,
index,
integer,
Expand All @@ -16,6 +17,7 @@ import {
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { zeroAddress } from "viem";

import { PLATINUM_PRODUCT_ID } from "@exactly/common/panda";

Expand All @@ -35,8 +37,13 @@ export const credentials = pgTable(
pandaId: text("panda_id"),
bridgeId: text("bridge_id"),
source: text("source"),
salt: text("salt").notNull().default(zeroAddress),
Comment thread
aguxez marked this conversation as resolved.
Comment thread
aguxez marked this conversation as resolved.
Comment thread
aguxez marked this conversation as resolved.
},
({ account, bridgeId }) => [uniqueIndex("account_index").on(account), uniqueIndex("bridge_id_index").on(bridgeId)],
({ account, bridgeId, salt }) => [
uniqueIndex("account_index").on(account),
uniqueIndex("bridge_id_index").on(bridgeId),
check("credentials_salt_hex_check", sql`${salt} ~ '^0x[0-9a-fA-F]{40}$'`),
Comment thread
aguxez marked this conversation as resolved.
],
);

export const cards = pgTable(
Expand Down
24 changes: 17 additions & 7 deletions server/hooks/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,17 @@ export default function hook({
);
const accounts = await database.query.credentials
.findMany({
columns: { account: true, publicKey: true, factory: true, source: true },
columns: { account: true, publicKey: true, factory: true, salt: true, source: true },
where: inArray(credentials.account, [...new Set(transfers.map(({ toAddress }) => toAddress))]),
})
.then((result) =>
Object.fromEntries(
result.map(
({ account, publicKey, factory, source }) =>
[v.parse(Address, account), { publicKey, factory: v.parse(Address, factory), source }] as const,
({ account, publicKey, factory, salt, source }) =>
[
v.parse(Address, account),
{ publicKey, factory: v.parse(Address, factory), salt: v.parse(Address, salt), source },
] as const,
),
),
);
Expand All @@ -144,7 +147,13 @@ export default function hook({
const markets = new Set(marketsByAsset.values());
const pokes = new Map<
Address,
{ assets: Set<Address>; factory: Address; publicKey: Uint8Array<ArrayBuffer>; source: null | string }
{
assets: Set<Address>;
factory: Address;
publicKey: Uint8Array<ArrayBuffer>;
salt: Address;
source: null | string;
}
>();
for (const { toAddress: account, rawContract, value, asset: assetSymbol } of transfers) {
if (!accounts[account]) continue;
Expand Down Expand Up @@ -180,19 +189,20 @@ export default function hook({
if (pokes.has(account)) {
pokes.get(account)?.assets.add(asset);
} else {
const { publicKey, factory, source } = accounts[account];
pokes.set(account, { publicKey, factory, source, assets: new Set([asset]) });
const { publicKey, factory, salt, source } = accounts[account];
pokes.set(account, { publicKey, factory, salt, source, assets: new Set([asset]) });
}
}
await Promise.all(
[...pokes].map(([account, { assets, factory, publicKey, source }]) =>
[...pokes].map(([account, { assets, factory, publicKey, salt, source }]) =>
poke.enqueue({
account,
assets: [...assets],
chainId: chain.id,
factory,
origin: "activity",
publicKey: bytesToHex(publicKey),
salt,
Comment thread
aguxez marked this conversation as resolved.
source,
}),
),
Expand Down
3 changes: 2 additions & 1 deletion server/hooks/persona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ export default function hook({
const { referenceId, fields } = attributes;

const credential = await database.query.credentials.findFirst({
columns: { account: true, factory: true, pandaId: true, publicKey: true, source: true },
columns: { account: true, factory: true, pandaId: true, publicKey: true, salt: true, source: true },
where: eq(credentials.id, referenceId),
});
if (!credential) {
Expand All @@ -361,6 +361,7 @@ export default function hook({
chainId: chain.id,
factory: parse(Address, current.factory),
publicKey: bytesToHex(current.publicKey),
salt: parse(Address, current.salt),
source: current.source,
});
}
Expand Down
6 changes: 5 additions & 1 deletion server/test/api/card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { SignJWT } from "jose";
import { createSecretKey } from "node:crypto";
import { env } from "node:process";
import { nonEmpty, parse, pipe, string } from "valibot";
import { checksumAddress, hexToBigInt, padHex, parseEther, zeroHash } from "viem";
import { checksumAddress, hexToBigInt, padHex, parseEther, zeroAddress, zeroHash } from "viem";
import { generatePrivateKey, privateKeyToAccount, privateKeyToAddress } from "viem/accounts";
import { base, optimism } from "viem/chains";
import { createSiweMessage, parseSiweMessage } from "viem/siwe";
Expand Down Expand Up @@ -2022,6 +2022,7 @@ describe("authenticated", () => {
},
assertion,
factory,
salt: zeroAddress,
statement,
});
});
Expand Down Expand Up @@ -2054,6 +2055,7 @@ describe("authenticated", () => {
credential: { publicKey: { type: "Buffer", data: [9, 8, 7] }, transports: null },
assertion,
factory,
salt: zeroAddress,
statement,
});
});
Expand Down Expand Up @@ -2110,6 +2112,7 @@ describe("authenticated", () => {
credential: { publicKey: { type: "Buffer", data: [1, 2, 3] }, transports: ["internal"] },
assertion,
factory,
salt: zeroAddress,
statement: `I authorize the account ${checksumAddress(account)} to be linked with the card ending in 4141 for my user (webauthn-panda-401-panda)`,
});
});
Expand Down Expand Up @@ -2143,6 +2146,7 @@ describe("authenticated", () => {
credential: { publicKey: { type: "Buffer", data: [4, 5, 6] }, transports: ["internal"] },
assertion,
factory,
salt: zeroAddress,
statement: `I authorize the account ${checksumAddress(account)} to be linked with the card ending in 5151 for my user (webauthn-panda-503-panda)`,
});
});
Expand Down
Loading
Loading