diff --git a/apps/customer_dashboard/src/app/users/forgot_password/page.tsx b/apps/customer_dashboard/src/app/users/forgot_password/page.tsx index 8444f6541..d1ed3d053 100644 --- a/apps/customer_dashboard/src/app/users/forgot_password/page.tsx +++ b/apps/customer_dashboard/src/app/users/forgot_password/page.tsx @@ -20,6 +20,8 @@ import { EMAIL_REGEX, EMAIL_VERIFICATION_TIMER_SECONDS, PASSWORD_MIN_LENGTH, + PASSWORD_MAX_LENGTH, + PASSWORD_CONTAINS_NUMBER_REGEX, SIX_DIGITS_REGEX, } from "@oko-wallet-ct-dashboard/constants"; import { ExpiryTimer } from "@oko-wallet-ct-dashboard/components/expiry_timer/expiry_timer"; @@ -49,6 +51,7 @@ export default function ForgotPasswordPage() { const [showPassword, setShowPassword] = useState(false); const [showConfirm, setShowConfirm] = useState(false); + const [isCodeExpired, setIsCodeExpired] = useState(false); const codeValue = useMemo(() => codeDigits.join(""), [codeDigits]); @@ -116,6 +119,14 @@ export default function ForgotPasswordPage() { setError(`Password must be at least ${PASSWORD_MIN_LENGTH} characters`); return; } + if (password.length > PASSWORD_MAX_LENGTH) { + setError(`Password must be at most ${PASSWORD_MAX_LENGTH} characters`); + return; + } + if (!PASSWORD_CONTAINS_NUMBER_REGEX.test(password)) { + setError("Password must include at least one number"); + return; + } if (password !== confirmPassword) { setError("Passwords do not match"); return; @@ -123,6 +134,7 @@ export default function ForgotPasswordPage() { setIsLoading(true); resetError(); + setIsCodeExpired(false); try { const res = await requestResetPasswordConfirm( email, @@ -132,7 +144,38 @@ export default function ForgotPasswordPage() { if (res.success) { router.push(paths.home); } else { - setError(res.msg || "Failed to reset password"); + if (res.code === "INVALID_VERIFICATION_CODE") { + setIsCodeExpired(true); + setError("Verification code has expired. Please request a new code."); + } else { + setError(res.msg || "Failed to reset password"); + } + } + } catch (err) { + setError("An unexpected error occurred"); + } finally { + setIsLoading(false); + } + }; + + const handleRequestNewCode = async () => { + if (!email || isLoading) { + return; + } + + setIsLoading(true); + resetError(); + setIsCodeExpired(false); + try { + const res = await requestForgotPassword(email); + if (res.success) { + setCodeDigits(EMPTY_CODE); + setVerifiedCode(""); + setPassword(""); + setConfirmPassword(""); + goToStep(Step.CODE); + } else { + setError(res.msg || "Failed to send code"); } } catch (err) { setError("An unexpected error occurred"); @@ -317,6 +360,7 @@ export default function ForgotPasswordPage() { }} fullWidth requiredSymbol + maxLength={16} helpText={ error ? undefined @@ -344,6 +388,7 @@ export default function ForgotPasswordPage() { }} fullWidth requiredSymbol + maxLength={16} SideComponent={ + {isCodeExpired ? ( + + ) : ( + + )} diff --git a/apps/customer_dashboard/src/components/reset_password/use_reset_password_form.ts b/apps/customer_dashboard/src/components/reset_password/use_reset_password_form.ts index 3729644e0..9fcb6bfb3 100644 --- a/apps/customer_dashboard/src/components/reset_password/use_reset_password_form.ts +++ b/apps/customer_dashboard/src/components/reset_password/use_reset_password_form.ts @@ -3,7 +3,11 @@ import { useForm, type SubmitHandler } from "react-hook-form"; import { useRouter } from "next/navigation"; import { paths } from "@oko-wallet-ct-dashboard/paths"; -import { PASSWORD_MIN_LENGTH } from "@oko-wallet-ct-dashboard/constants"; +import { + PASSWORD_MIN_LENGTH, + PASSWORD_MAX_LENGTH, + PASSWORD_CONTAINS_NUMBER_REGEX, +} from "@oko-wallet-ct-dashboard/constants"; import { requestChangePassword } from "@oko-wallet-ct-dashboard/fetch/users"; import { useAppState } from "@oko-wallet-ct-dashboard/state"; @@ -91,6 +95,16 @@ function resetPasswordResolver(values: ResetPasswordInputs) { type: "minLength", message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters`, }; + } else if (values.newPassword.length > PASSWORD_MAX_LENGTH) { + errors.newPassword = { + type: "maxLength", + message: `Password must be at most ${PASSWORD_MAX_LENGTH} characters`, + }; + } else if (!PASSWORD_CONTAINS_NUMBER_REGEX.test(values.newPassword)) { + errors.newPassword = { + type: "pattern", + message: "Password must include at least one number", + }; } if (!values.confirmPassword) { diff --git a/apps/customer_dashboard/src/constants/index.ts b/apps/customer_dashboard/src/constants/index.ts index 69623fce9..c0b4a6ca0 100644 --- a/apps/customer_dashboard/src/constants/index.ts +++ b/apps/customer_dashboard/src/constants/index.ts @@ -1,8 +1,10 @@ export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export const PASSWORD_MIN_LENGTH = 8; +export const PASSWORD_MAX_LENGTH = 16; +export const PASSWORD_CONTAINS_NUMBER_REGEX = /\d/; export const SIX_DIGITS_REGEX = /^\d{6}$/; -export const EMAIL_VERIFICATION_TIMER_SECONDS = 60 * 3; +export const EMAIL_VERIFICATION_TIMER_SECONDS = 60; export const GET_STARTED_URL = "https://form.typeform.com/to/MxrBGq9b"; diff --git a/apps/docs_web/docs/v0/sdk-usage/cosmos-kit-integration.md b/apps/docs_web/docs/v0/sdk-usage/cosmos-kit-integration.md index 226ac76e7..35bd1b0c7 100644 --- a/apps/docs_web/docs/v0/sdk-usage/cosmos-kit-integration.md +++ b/apps/docs_web/docs/v0/sdk-usage/cosmos-kit-integration.md @@ -36,21 +36,16 @@ npm install @cosmjs/amino @cosmjs/proto-signing ## Basic Setup -### 1. Create Oko Wallets +### 1. Create Oko Wallet -Use `makeOkoWallets` to generate wallet instances for your desired login -providers: +Use `makeOkoWallet` to create a wallet instance: ```typescript -import { makeOkoWallets } from "@oko-wallet/oko-cosmos-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-cosmos-kit"; -const okoWallets = makeOkoWallets({ +const okoWallet = makeOkoWallet({ apiKey: "your-oko-api-key", sdkEndpoint: "https://your-custom-oko-sdk.example.com", // optional, Only specify if you have your own SDK endpoint - loginMethods: [ - { provider: "google" }, - // optional, Specify which login providers to enable. If not specified, all available providers will be included - ], }); ``` @@ -61,10 +56,10 @@ wallets: ```typescript import { ChainProvider } from "@cosmos-kit/react"; -import { makeOkoWallets } from "@oko-wallet/oko-cosmos-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-cosmos-kit"; import { chains, assets } from "chain-registry"; -const okoWallets = makeOkoWallets({ +const okoWallet = makeOkoWallet({ apiKey: "your-oko-api-key", }); @@ -73,7 +68,7 @@ export default function App({ Component, pageProps }) { @@ -116,7 +111,7 @@ function WalletConnect() { ### OkoWalletOptions -The `makeOkoWallets` function accepts the following options: +The `makeOkoWallet` function accepts the following options: ```typescript interface OkoWalletOptions { @@ -126,31 +121,18 @@ interface OkoWalletOptions { // Custom SDK endpoint (optional) // Defaults to Oko's production endpoint sdkEndpoint?: string; - - // Login methods to enable (optional) - // If not specified, all available providers will be enabled - loginMethods?: OkoLoginMethod[]; -} - -interface OkoLoginMethod { - provider: SignInType; } - -type SignInType = "google" | "email" | "x" | "telegram" | "discord"; ``` ### Login Providers -Each login provider creates a separate wallet entry in Cosmos Kit's wallet list: - -- `oko_wallet_google` - Google OAuth login -- `oko_wallet_email` - Email/passwordless login -- `oko_wallet_x` - X (Twitter) OAuth login -- `oko_wallet_telegram` - Telegram login -- `oko_wallet_discord` - Discord OAuth login +When connecting, users can select their preferred login provider from a modal: -When `loginMethods` is not specified, all available providers are automatically -included. +- Google OAuth +- Email/passwordless +- X (Twitter) OAuth +- Telegram OAuth +- Discord OAuth ## Next Steps diff --git a/apps/docs_web/docs/v0/sdk-usage/interchain-kit-integration.md b/apps/docs_web/docs/v0/sdk-usage/interchain-kit-integration.md index 69ffcdfab..7ced93fd7 100644 --- a/apps/docs_web/docs/v0/sdk-usage/interchain-kit-integration.md +++ b/apps/docs_web/docs/v0/sdk-usage/interchain-kit-integration.md @@ -33,22 +33,16 @@ npm install @oko-wallet/oko-interchain-kit @interchain-kit/react @interchain-kit ## Basic Setup -### 1. Create Oko Wallets +### 1. Create Oko Wallet -Use `makeOkoWallets` to generate wallet instances for your desired login -providers: +Use `makeOkoWallet` to create a wallet instance: ```typescript -import { makeOkoWallets } from "@oko-wallet/oko-interchain-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-interchain-kit"; -const okoWallets = makeOkoWallets({ +const okoWallet = makeOkoWallet({ apiKey: "your-oko-api-key", sdkEndpoint: "https://your-custom-oko-sdk.example.com", // optional, Only specify if you have your own SDK endpoint - loginMethods: [ - { provider: "google" }, - { provider: "email" }, - // optional, Specify which login providers to enable. If not specified, all available providers will be included - ], }); ``` @@ -59,10 +53,10 @@ wallets: ```typescript import { ChainProvider } from "@interchain-kit/react"; -import { makeOkoWallets } from "@oko-wallet/oko-interchain-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-interchain-kit"; import { chains, assetLists } from "@chain-registry/v2"; -const okoWallets = makeOkoWallets({ +const okoWallet = makeOkoWallet({ apiKey: "your-oko-api-key", }); @@ -71,7 +65,7 @@ export default function App({ Component, pageProps }) { @@ -109,7 +103,7 @@ function WalletConnect() { ### OkoWalletOptions -The `makeOkoWallets` function accepts the following options: +The `makeOkoWallet` function accepts the following options: ```typescript interface OkoWalletOptions { @@ -119,32 +113,18 @@ interface OkoWalletOptions { // Custom SDK endpoint (optional) // Defaults to Oko's production endpoint sdkEndpoint?: string; - - // Login methods to enable (optional) - // If not specified, all available providers will be enabled - loginMethods?: OkoLoginMethod[]; -} - -interface OkoLoginMethod { - provider: SignInType; } - -type SignInType = "google" | "email" | "x" | "telegram" | "discord"; ``` ### Login Providers -Each login provider creates a separate wallet entry in Interchain Kit's wallet -list: - -- `oko-wallet_google` - Google OAuth login -- `oko-wallet_email` - Email/passwordless login -- `oko-wallet_x` - X (Twitter) OAuth login -- `oko-wallet_telegram` - Telegram login -- `oko-wallet_discord` - Discord OAuth login +When connecting, users can select their preferred login provider from a modal: -When `loginMethods` is not specified, all available providers are automatically -included. +- Google OAuth +- Email/passwordless +- X (Twitter) OAuth +- Telegram OAuth +- Discord OAuth ## Signing Transactions diff --git a/apps/email_template_2/src/app/reset_pw_code/page.tsx b/apps/email_template_2/src/app/reset_pw_code/page.tsx index 62736b22c..a03f4eee7 100644 --- a/apps/email_template_2/src/app/reset_pw_code/page.tsx +++ b/apps/email_template_2/src/app/reset_pw_code/page.tsx @@ -72,7 +72,7 @@ export default function ResetPwCodePage() { password.
The code is valid for{" "} - {"${email_verification_expiration_minutes}"} minutes + {"${email_verification_expiration_minutes}"} minute for your security. diff --git a/backend/ct_dashboard_api/src/constants/index.ts b/backend/ct_dashboard_api/src/constants/index.ts index 3ffd04649..3ee504128 100644 --- a/backend/ct_dashboard_api/src/constants/index.ts +++ b/backend/ct_dashboard_api/src/constants/index.ts @@ -1,6 +1,8 @@ export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export const PASSWORD_MIN_LENGTH = 4; export const CHANGED_PASSWORD_MIN_LENGTH = 8; +export const CHANGED_PASSWORD_MAX_LENGTH = 16; +export const PASSWORD_CONTAINS_NUMBER_REGEX = /\d/; export const SIX_DIGITS_REGEX = /^\d{6}$/; diff --git a/backend/ct_dashboard_api/src/email/password_reset.ts b/backend/ct_dashboard_api/src/email/password_reset.ts index 543911fd1..b30518921 100644 --- a/backend/ct_dashboard_api/src/email/password_reset.ts +++ b/backend/ct_dashboard_api/src/email/password_reset.ts @@ -12,7 +12,7 @@ export async function sendPasswordResetEmail( const subject = `Reset Password Verification Code for ${customer_label}`; const html = ` - Oko Email Template
Oko password reset code header

Enter this code in your Oko Dashboard to reset your password.
The code is valid for ${email_verification_expiration_minutes} minutes for your security.

 

Your 6-digit code
for changing your password

 

${verification_code}

 
 

If you didn't make this request, you can safely delete this email.

 

Oko Team

 
Gray Oko logo
+ Oko Email Template
Oko password reset code header

Enter this code in your Oko Dashboard to reset your password.
The code is valid for ${email_verification_expiration_minutes} minute for your security.

 

Your 6-digit code
for changing your password

 

${verification_code}

 
 

If you didn't make this request, you can safely delete this email.

 

Oko Team

 
Gray Oko logo
`; console.info( diff --git a/backend/ct_dashboard_api/src/routes/customer_auth.ts b/backend/ct_dashboard_api/src/routes/customer_auth.ts index d2a29a18c..4d67fe0e8 100644 --- a/backend/ct_dashboard_api/src/routes/customer_auth.ts +++ b/backend/ct_dashboard_api/src/routes/customer_auth.ts @@ -17,7 +17,10 @@ import { getCTDUserWithCustomerByEmail, } from "@oko-wallet/oko-pg-interface/customer_dashboard_users"; import { hashPassword, comparePassword } from "@oko-wallet/crypto-js"; -import { verifyEmailCode } from "@oko-wallet/oko-pg-interface/email_verifications"; +import { + verifyEmailCode, + markCodeVerified, +} from "@oko-wallet/oko-pg-interface/email_verifications"; import { registry } from "@oko-wallet/oko-api-openapi"; import { ErrorResponseSchema } from "@oko-wallet/oko-api-openapi/common"; import { @@ -41,6 +44,8 @@ import { generateCustomerToken } from "@oko-wallet-ctd-api/auth"; import { sendEmailVerificationCode } from "@oko-wallet-ctd-api/email/send"; import { CHANGED_PASSWORD_MIN_LENGTH, + CHANGED_PASSWORD_MAX_LENGTH, + PASSWORD_CONTAINS_NUMBER_REGEX, EMAIL_REGEX, SIX_DIGITS_REGEX, } from "@oko-wallet-ctd-api/constants"; @@ -51,10 +56,7 @@ import { import { rateLimitMiddleware } from "@oko-wallet-ctd-api/middleware/rate_limit"; import { generateVerificationCode } from "@oko-wallet-ctd-api/email/verification"; import { sendPasswordResetEmail } from "@oko-wallet-ctd-api/email/password_reset"; -import { - createEmailVerification, - getLatestPendingVerification, -} from "@oko-wallet/oko-pg-interface/email_verifications"; +import { createEmailVerification } from "@oko-wallet/oko-pg-interface/email_verifications"; export function setCustomerAuthRoutes(router: Router) { registry.registerPath({ @@ -291,16 +293,8 @@ export function setCustomerAuthRoutes(router: Router) { return; } - const pendingRes = await getLatestPendingVerification(state.db, email); - if (!pendingRes.success) { - res - .status(500) - .json({ success: false, code: "UNKNOWN_ERROR", msg: "DB Error" }); - return; - } - - const pending = pendingRes.data; - if (!pending || pending.verification_code !== code) { + const result = await markCodeVerified(state.db, email, code, 5); + if (!result.success) { res.status(400).json({ success: false, code: "INVALID_VERIFICATION_CODE", @@ -402,6 +396,24 @@ export function setCustomerAuthRoutes(router: Router) { return; } + if (newPassword.length > CHANGED_PASSWORD_MAX_LENGTH) { + res.status(400).json({ + success: false, + code: "INVALID_EMAIL_OR_PASSWORD", + msg: "Password too long", + }); + return; + } + + if (!PASSWORD_CONTAINS_NUMBER_REGEX.test(newPassword)) { + res.status(400).json({ + success: false, + code: "INVALID_EMAIL_OR_PASSWORD", + msg: "Password must include at least one number", + }); + return; + } + const verificationResult = await verifyEmailCode(state.db, { email, verification_code: code, @@ -1027,6 +1039,24 @@ export function setCustomerAuthRoutes(router: Router) { return; } + if (request.new_password.length > CHANGED_PASSWORD_MAX_LENGTH) { + res.status(400).json({ + success: false, + code: "INVALID_EMAIL_OR_PASSWORD", + msg: "Password must be at most 16 characters long", + }); + return; + } + + if (!PASSWORD_CONTAINS_NUMBER_REGEX.test(request.new_password)) { + res.status(400).json({ + success: false, + code: "INVALID_EMAIL_OR_PASSWORD", + msg: "Password must include at least one number", + }); + return; + } + // Inline changePassword logic const customerAccountResult = await getCTDUserWithCustomerAndPasswordHashByEmail( diff --git a/backend/oko_pg_interface/src/email_verifications/index.ts b/backend/oko_pg_interface/src/email_verifications/index.ts index ec9267b9e..dd5451ea7 100644 --- a/backend/oko_pg_interface/src/email_verifications/index.ts +++ b/backend/oko_pg_interface/src/email_verifications/index.ts @@ -15,11 +15,11 @@ export async function createEmailVerification( ): Promise> { const query = ` INSERT INTO email_verifications ( - email_verification_id, email, verification_code, + email_verification_id, email, verification_code, status, expires_at ) VALUES ( - $1, $2, $3, + $1, $2, $3, $4, $5 ) RETURNING * @@ -61,14 +61,14 @@ export async function verifyEmailCode( ): Promise> { try { const updateQuery = ` -UPDATE email_verifications +UPDATE email_verifications SET status = '${EmailVerificationStatus.VERIFIED}', updated_at = NOW() WHERE email_verification_id = ( SELECT email_verification_id FROM email_verifications WHERE email = $1 AND verification_code = $2 - AND status = '${EmailVerificationStatus.PENDING}' + AND status = '${EmailVerificationStatus.CODE_VERIFIED}' AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1 @@ -110,14 +110,65 @@ RETURNING status } } +export async function markCodeVerified( + db: Pool, + email: string, + code: string, + extendMinutes: number = 5, +): Promise> { + const query = ` +UPDATE email_verifications +SET status = '${EmailVerificationStatus.CODE_VERIFIED}', + expires_at = NOW() + INTERVAL '1 minute' * $3, + updated_at = NOW() +WHERE email_verification_id = ( + SELECT email_verification_id + FROM email_verifications + WHERE email = $1 + AND verification_code = $2 + AND status = '${EmailVerificationStatus.PENDING}' + AND expires_at > NOW() + ORDER BY created_at DESC + LIMIT 1 +) +RETURNING * +`; + + try { + const result = await db.query(query, [ + email, + code, + extendMinutes, + ]); + + const row = result.rows[0]; + if (!row) { + return { + success: false, + err: "Invalid or expired verification code", + }; + } + + return { + success: true, + data: row, + }; + } catch (error) { + return { + success: false, + err: String(error), + }; + } +} + export async function getLatestPendingVerification( db: Pool, email: string, ): Promise> { const query = ` -SELECT * +SELECT * FROM email_verifications -WHERE email = $1 +WHERE email = $1 AND status = '${EmailVerificationStatus.PENDING}' AND expires_at > NOW() ORDER BY created_at DESC diff --git a/backend/openapi/src/ct_dashboard/customer_auth.ts b/backend/openapi/src/ct_dashboard/customer_auth.ts index b37458237..2c1698679 100644 --- a/backend/openapi/src/ct_dashboard/customer_auth.ts +++ b/backend/openapi/src/ct_dashboard/customer_auth.ts @@ -118,9 +118,15 @@ export const ChangePasswordRequestSchema = registry.register( email: z.email().openapi({ description: "Email address of the account", }), - new_password: z.string().min(8).openapi({ - description: "New password to set", - }), + new_password: z + .string() + .min(8) + .max(16) + .regex(/\d/, "Password must include at least one number") + .openapi({ + description: + "New password to set (8-16 characters, must include at least one number)", + }), original_password: z.string().optional().openapi({ description: "Current password for verification", }), @@ -219,9 +225,15 @@ export const ResetPasswordConfirmRequestSchema = registry.register( code: z.string().length(6).openapi({ description: "The 6-digit verification code", }), - newPassword: z.string().min(8).openapi({ - description: "The new password", - }), + newPassword: z + .string() + .min(8) + .max(16) + .regex(/\d/, "Password must include at least one number") + .openapi({ + description: + "The new password (8-16 characters, must include at least one number)", + }), }), ); diff --git a/backend/tss_api/src/api/keygen_ed25519/index.test.ts b/backend/tss_api/src/api/keygen_ed25519/index.test.ts index c748e8d24..a48a402eb 100644 --- a/backend/tss_api/src/api/keygen_ed25519/index.test.ts +++ b/backend/tss_api/src/api/keygen_ed25519/index.test.ts @@ -5,9 +5,14 @@ import { runKeygenCentralizedEd25519 } from "@oko-wallet/teddsa-addon/src/server import { createPgConn } from "@oko-wallet/postgres-lib"; import { insertKSNode } from "@oko-wallet/oko-pg-interface/ks_nodes"; import { createUser } from "@oko-wallet/oko-pg-interface/oko_users"; -import { createWallet } from "@oko-wallet/oko-pg-interface/oko_wallets"; +import { + createWallet, + getWalletById, +} from "@oko-wallet/oko-pg-interface/oko_wallets"; import { insertKeyShareNodeMeta } from "@oko-wallet/oko-pg-interface/key_share_node_meta"; import type { WalletStatus } from "@oko-wallet/oko-types/wallets"; +import { decryptDataAsync } from "@oko-wallet/crypto-js/node"; +import { extractKeyPackageSharesEd25519 } from "@oko-wallet/teddsa-addon/src/server"; import { resetPgDatabase } from "@oko-wallet-tss-api/testing/database"; import { testPgConfig } from "@oko-wallet-tss-api/database/test_config"; @@ -106,6 +111,12 @@ describe("Ed25519 Keygen", () => { TEMP_ENC_SECRET, ); + if (!result.success) { + console.error("Keygen failed:", { + code: result.code, + msg: result.msg, + }); + } expect(result.success).toBe(true); if (result.success) { expect(result.data.token).toBeDefined(); @@ -342,5 +353,70 @@ describe("Ed25519 Keygen", () => { // In production, the user_id would come from the database } }); + + it("should store only signing_share and verifying_share in enc_tss_share", async () => { + await setUpKSNodes(pool); + await setUpKeyShareNodeMeta(pool); + + const keygenResult = runKeygenCentralizedEd25519(); + const serverKeygenOutput = keygenResult.keygen_outputs[Participant.P1]; + + // Extract expected shares from server key_package + const expectedShares = extractKeyPackageSharesEd25519( + new Uint8Array(serverKeygenOutput.key_package), + ); + + const request = generateKeygenRequest(keygenResult); + + const result = await runKeygenEd25519( + pool, + TEST_JWT_CONFIG, + request, + TEMP_ENC_SECRET, + ); + + expect(result.success).toBe(true); + if (result.success) { + // Get wallet from database using wallet_id from result + const getWalletRes = await getWalletById( + pool, + result.data.user.wallet_id, + ); + expect(getWalletRes.success).toBe(true); + if (getWalletRes.success && getWalletRes.data) { + const wallet = getWalletRes.data; + + // Decrypt enc_tss_share + const encryptedShare = wallet.enc_tss_share.toString("utf-8"); + const decryptedShare = await decryptDataAsync( + encryptedShare, + TEMP_ENC_SECRET, + ); + const storedShares = JSON.parse(decryptedShare) as { + signing_share: number[]; + verifying_share: number[]; + }; + + // Verify structure: should only have signing_share and verifying_share + expect(storedShares).toHaveProperty("signing_share"); + expect(storedShares).toHaveProperty("verifying_share"); + expect(storedShares).not.toHaveProperty("key_package"); + expect(storedShares).not.toHaveProperty("public_key_package"); + expect(storedShares).not.toHaveProperty("identifier"); + + // Verify sizes: each should be 32 bytes + expect(storedShares.signing_share).toHaveLength(32); + expect(storedShares.verifying_share).toHaveLength(32); + + // Verify values match expected shares + expect(storedShares.signing_share).toEqual( + expectedShares.signing_share, + ); + expect(storedShares.verifying_share).toEqual( + expectedShares.verifying_share, + ); + } + } + }); }); }); diff --git a/backend/tss_api/src/api/keygen_ed25519/index.ts b/backend/tss_api/src/api/keygen_ed25519/index.ts index dabfdd221..30eca01d3 100644 --- a/backend/tss_api/src/api/keygen_ed25519/index.ts +++ b/backend/tss_api/src/api/keygen_ed25519/index.ts @@ -21,6 +21,7 @@ import { import { getKeyShareNodeMeta } from "@oko-wallet/oko-pg-interface/key_share_node_meta"; import { generateUserToken } from "@oko-wallet-tss-api/api/keplr_auth"; +import { extractKeyPackageSharesEd25519 } from "@oko-wallet/teddsa-addon/src/server"; export async function runKeygenEd25519( db: Pool, @@ -114,10 +115,7 @@ export async function runKeygenEd25519( }; } - // Ed25519 uses 2-of-2 threshold signature with server, not SSS key share - // nodes - // Skip checkKeyShareFromKSNodes validation (which expects secp256k1 - // 33-byte keys) + // TODO: Add KS node check after SSS & KSN logic is implemented const getActiveKSNodesRes = await getActiveKSNodes(db); if (getActiveKSNodesRes.success === false) { return { @@ -129,14 +127,19 @@ export async function runKeygenEd25519( const activeKSNodes = getActiveKSNodesRes.data; const ksNodeIds: string[] = activeKSNodes.map((node) => node.node_id); - const keyPackageJson = JSON.stringify({ - key_package: keygen_2.key_package, - public_key_package: keygen_2.public_key_package, - identifier: keygen_2.identifier, - }); + // Extract signing_share and verifying_share from key_package + const keyPackageShares = extractKeyPackageSharesEd25519( + new Uint8Array(keygen_2.key_package), + ); + + // Store only signing_share and verifying_share (64 bytes total) + const sharesData = { + signing_share: keyPackageShares.signing_share, + verifying_share: keyPackageShares.verifying_share, + }; const encryptedShare = await encryptDataAsync( - keyPackageJson, + JSON.stringify(sharesData), encryptionSecret, ); const encryptedShareBuffer = Buffer.from(encryptedShare, "utf-8"); diff --git a/backend/tss_api/src/api/presign_ed25519/index.ts b/backend/tss_api/src/api/presign_ed25519/index.ts deleted file mode 100644 index 71c649019..000000000 --- a/backend/tss_api/src/api/presign_ed25519/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - createTssSession, - createTssStage, -} from "@oko-wallet/oko-pg-interface/tss"; -import type { - PresignEd25519Request, - PresignEd25519Response, - PresignEd25519StageData, -} from "@oko-wallet/oko-types/tss"; -import { - TssStageType, - PresignEd25519StageStatus, -} from "@oko-wallet/oko-types/tss"; -import type { KeygenEd25519Output } from "@oko-wallet/oko-types/tss"; -import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; -import { Pool } from "pg"; -import { decryptDataAsync } from "@oko-wallet/crypto-js/node"; -import { runSignRound1Ed25519 } from "@oko-wallet/teddsa-addon/src/server"; - -import { validateWalletEmail } from "@oko-wallet-tss-api/api/utils"; - -export async function runPresignEd25519( - db: Pool, - encryptionSecret: string, - request: PresignEd25519Request, -): Promise> { - try { - const { email, wallet_id, customer_id } = request; - - const validateWalletEmailRes = await validateWalletEmail( - db, - wallet_id, - email, - ); - if (validateWalletEmailRes.success === false) { - return { - success: false, - code: "UNAUTHORIZED", - msg: validateWalletEmailRes.err, - }; - } - const wallet = validateWalletEmailRes.data; - - if (wallet.curve_type !== "ed25519") { - return { - success: false, - code: "INVALID_WALLET_TYPE", - msg: `Wallet is not ed25519 type: ${wallet.curve_type}`, - }; - } - - const encryptedShare = wallet.enc_tss_share.toString("utf-8"); - const decryptedShare = await decryptDataAsync( - encryptedShare, - encryptionSecret, - ); - const keygenOutput: KeygenEd25519Output = JSON.parse(decryptedShare); - - // Generate nonces and commitments (Round 1 without message) - const round1Result = runSignRound1Ed25519( - new Uint8Array(keygenOutput.key_package), - ); - - // Create TSS session - const sessionRes = await createTssSession(db, { - customer_id, - wallet_id, - }); - if (!sessionRes.success) { - return { - success: false, - code: "UNKNOWN_ERROR", - msg: `Failed to create TSS session: ${sessionRes.err}`, - }; - } - const session = sessionRes.data; - - // Create TSS stage with presign data (nonces stored for later use) - const stageData: PresignEd25519StageData = { - nonces: round1Result.nonces, - identifier: round1Result.identifier, - commitments: round1Result.commitments, - }; - - const stageRes = await createTssStage(db, { - session_id: session.session_id, - stage_type: TssStageType.PRESIGN_ED25519, - stage_status: PresignEd25519StageStatus.COMPLETED, - stage_data: stageData, - }); - if (!stageRes.success) { - return { - success: false, - code: "UNKNOWN_ERROR", - msg: `Failed to create TSS stage: ${stageRes.err}`, - }; - } - - return { - success: true, - data: { - session_id: session.session_id, - commitments_0: { - identifier: round1Result.identifier, - commitments: round1Result.commitments, - }, - }, - }; - } catch (error) { - return { - success: false, - code: "UNKNOWN_ERROR", - msg: `runPresignEd25519 error: ${error instanceof Error ? error.message : String(error)}`, - }; - } -} diff --git a/backend/tss_api/src/api/sign_ed25519/index.test.ts b/backend/tss_api/src/api/sign_ed25519/index.test.ts index 3ea8ebd46..0f16d68cd 100644 --- a/backend/tss_api/src/api/sign_ed25519/index.test.ts +++ b/backend/tss_api/src/api/sign_ed25519/index.test.ts @@ -1,711 +1,868 @@ -// TODO: refactor this tests @chemonoworld - -// import { jest } from "@jest/globals"; -// import { Pool } from "pg"; -// import type { -// SignEd25519Round1Request, -// SignEd25519Round2Request, -// SignEd25519AggregateRequest, -// } from "@oko-wallet/oko-types/tss"; -// import type { TeddsaKeygenOutput } from "@oko-wallet/teddsa-interface"; -// import { Participant } from "@oko-wallet/teddsa-interface"; -// import { -// runKeygenCentralizedEd25519, -// runSignRound1Ed25519 as clientRunSignRound1Ed25519, -// runSignRound2Ed25519 as clientRunSignRound2Ed25519, -// runAggregateEd25519 as clientRunAggregateEd25519, -// runVerifyEd25519, -// } from "@oko-wallet/teddsa-addon/src/server"; -// import { createPgConn } from "@oko-wallet/postgres-lib"; -// import type { WalletStatus } from "@oko-wallet/oko-types/wallets"; -// import { insertKSNode } from "@oko-wallet/oko-pg-interface/ks_nodes"; -// import { createWallet } from "@oko-wallet/oko-pg-interface/oko_wallets"; -// import { createUser } from "@oko-wallet/oko-pg-interface/oko_users"; -// import { insertKeyShareNodeMeta } from "@oko-wallet/oko-pg-interface/key_share_node_meta"; -// import { insertCustomer } from "@oko-wallet/oko-pg-interface/customers"; -// import { encryptDataAsync } from "@oko-wallet/crypto-js/node"; - -// import { resetPgDatabase } from "@oko-wallet-tss-api/testing/database"; -// import { testPgConfig } from "@oko-wallet-tss-api/database/test_config"; -// import { -// runSignEd25519Round1, -// runSignEd25519Round2, -// runSignEd25519Aggregate, -// } from "@oko-wallet-tss-api/api/sign_ed25519"; -// import { TEMP_ENC_SECRET } from "@oko-wallet-tss-api/api/utils"; - -// const SSS_THRESHOLD = 2; -// const TEST_EMAIL = "test-ed25519@test.com"; - -// interface TestSetupResult { -// pool: Pool; -// walletId: string; -// customerId: string; -// clientKeygenOutput: TeddsaKeygenOutput; -// serverKeygenOutput: TeddsaKeygenOutput; -// } - -// async function setUpKSNodes(pool: Pool): Promise { -// const ksNodeNames = ["ksNode1", "ksNode2"]; -// const ksNodeIds = []; -// const createKSNodesRes = await Promise.all( -// ksNodeNames.map((ksNodeName) => -// insertKSNode(pool, ksNodeName, `http://test.com/${ksNodeName}`), -// ), -// ); -// for (const res of createKSNodesRes) { -// if (res.success === false) { -// throw new Error("Failed to create ks nodes"); -// } -// ksNodeIds.push(res.data.node_id); -// } -// return ksNodeIds; -// } - -// async function setUpEd25519Wallet(pool: Pool): Promise { -// // Generate keys using centralized keygen -// const keygenResult = runKeygenCentralizedEd25519(); -// const clientKeygenOutput = keygenResult.keygen_outputs[Participant.P0]; -// const serverKeygenOutput = keygenResult.keygen_outputs[Participant.P1]; - -// // Set up KS nodes and metadata -// const ksNodeIds = await setUpKSNodes(pool); -// await insertKeyShareNodeMeta(pool, { -// sss_threshold: SSS_THRESHOLD, -// }); - -// // Create customer -// const customerId = crypto.randomUUID(); -// const insertCustomerRes = await insertCustomer(pool, { -// customer_id: customerId, -// label: "test-customer", -// status: "ACTIVE", -// url: null, -// logo_url: null, -// theme: "dark", -// }); -// if (insertCustomerRes.success === false) { -// throw new Error(`Failed to create customer: ${insertCustomerRes.err}`); -// } - -// // Create user -// const createUserRes = await createUser(pool, TEST_EMAIL, "google"); -// if (createUserRes.success === false) { -// throw new Error(`Failed to create user: ${createUserRes.err}`); -// } -// const userId = createUserRes.data.user_id; - -// // Encrypt server key package -// const serverKeyPackageJson = JSON.stringify(serverKeygenOutput); -// const encryptedShare = await encryptDataAsync( -// serverKeyPackageJson, -// TEMP_ENC_SECRET, -// ); - -// // Create Ed25519 wallet -// const createWalletRes = await createWallet(pool, { -// user_id: userId, -// curve_type: "ed25519", -// public_key: Buffer.from(keygenResult.public_key), -// enc_tss_share: Buffer.from(encryptedShare, "utf-8"), -// sss_threshold: SSS_THRESHOLD, -// status: "ACTIVE" as WalletStatus, -// }); -// if (createWalletRes.success === false) { -// throw new Error(`Failed to create wallet: ${createWalletRes.err}`); -// } -// const walletId = createWalletRes.data.wallet_id; - -// return { -// pool, -// walletId, -// customerId, -// clientKeygenOutput, -// serverKeygenOutput, -// }; -// } - -// describe("Ed25519 Signing", () => { -// let pool: Pool; - -// beforeAll(async () => { -// const config = testPgConfig; -// const createPostgresRes = await createPgConn({ -// database: config.database, -// host: config.host, -// password: config.password, -// user: config.user, -// port: config.port, -// ssl: config.ssl, -// }); - -// if (createPostgresRes.success === false) { -// console.error(createPostgresRes.err); -// throw new Error("Failed to create postgres database"); -// } - -// pool = createPostgresRes.data; -// }); - -// afterAll(async () => { -// await pool.end(); -// }); - -// beforeEach(async () => { -// await resetPgDatabase(pool); -// }); - -// describe("runSignEd25519Round1", () => { -// it("should generate commitments successfully", async () => { -// const { walletId, customerId } = await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message"); - -// const request: SignEd25519Round1Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...testMessage], -// }; - -// const result = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, request); - -// expect(result.success).toBe(true); -// if (result.success) { -// expect(result.data.session_id).toBeDefined(); -// expect(result.data.commitments_0).toBeDefined(); -// expect(result.data.commitments_0.identifier).toBeDefined(); -// expect(result.data.commitments_0.commitments).toBeDefined(); -// expect(Array.isArray(result.data.commitments_0.identifier)).toBe(true); -// expect(Array.isArray(result.data.commitments_0.commitments)).toBe(true); -// } -// }); - -// it("should fail with invalid email", async () => { -// const { walletId, customerId } = await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message"); - -// const request: SignEd25519Round1Request = { -// email: "wrong@test.com", -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...testMessage], -// }; - -// const result = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, request); - -// expect(result.success).toBe(false); -// if (!result.success) { -// expect(result.code).toBe("UNAUTHORIZED"); -// } -// }); - -// it("should fail with invalid wallet_id", async () => { -// const { customerId } = await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message"); - -// const request: SignEd25519Round1Request = { -// email: TEST_EMAIL, -// wallet_id: "00000000-0000-0000-0000-000000000000", -// customer_id: customerId, -// msg: [...testMessage], -// }; - -// const result = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, request); - -// expect(result.success).toBe(false); -// if (!result.success) { -// expect(result.code).toBe("UNAUTHORIZED"); -// } -// }); -// }); - -// describe("runSignEd25519Round2", () => { -// it("should generate signature share successfully", async () => { -// const { walletId, customerId, clientKeygenOutput } = -// await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message for Ed25519"); - -// // Round 1: Get server commitments -// const round1Request: SignEd25519Round1Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...testMessage], -// }; -// const round1Result = await runSignEd25519Round1( -// pool, -// TEMP_ENC_SECRET, -// round1Request, -// ); -// expect(round1Result.success).toBe(true); -// if (!round1Result.success) throw new Error("Round 1 failed"); - -// // Client generates their round 1 output -// const clientRound1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); - -// // Round 2: Get server signature share -// const round2Request: SignEd25519Round2Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: round1Result.data.session_id, -// commitments_1: { -// identifier: clientRound1.identifier, -// commitments: clientRound1.commitments, -// }, -// }; -// const round2Result = await runSignEd25519Round2( -// pool, -// TEMP_ENC_SECRET, -// round2Request, -// ); - -// expect(round2Result.success).toBe(true); -// if (round2Result.success) { -// expect(round2Result.data.signature_share_0).toBeDefined(); -// expect(round2Result.data.signature_share_0.identifier).toBeDefined(); -// expect( -// round2Result.data.signature_share_0.signature_share, -// ).toBeDefined(); -// } -// }); - -// it("should fail with invalid session_id", async () => { -// const { walletId, customerId, clientKeygenOutput } = -// await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message"); - -// const clientRound1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); - -// const round2Request: SignEd25519Round2Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: "00000000-0000-0000-0000-000000000000", -// commitments_1: { -// identifier: clientRound1.identifier, -// commitments: clientRound1.commitments, -// }, -// }; -// const result = await runSignEd25519Round2( -// pool, -// TEMP_ENC_SECRET, -// round2Request, -// ); - -// expect(result.success).toBe(false); -// if (!result.success) { -// expect(result.code).toBe("INVALID_TSS_SESSION"); -// } -// }); - -// it("should fail when Round2 is called twice (duplicate call prevention)", async () => { -// const { walletId, customerId, clientKeygenOutput } = -// await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message"); - -// // Round 1: Get server commitments -// const round1Request: SignEd25519Round1Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...testMessage], -// }; -// const round1Result = await runSignEd25519Round1( -// pool, -// TEMP_ENC_SECRET, -// round1Request, -// ); -// expect(round1Result.success).toBe(true); -// if (!round1Result.success) throw new Error("Round 1 failed"); - -// const clientRound1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); - -// // First Round 2 call - should succeed -// const round2Request: SignEd25519Round2Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: round1Result.data.session_id, -// commitments_1: { -// identifier: clientRound1.identifier, -// commitments: clientRound1.commitments, -// }, -// }; -// const round2Result = await runSignEd25519Round2( -// pool, -// TEMP_ENC_SECRET, -// round2Request, -// ); -// expect(round2Result.success).toBe(true); - -// // Second Round 2 call with same session - should fail -// const duplicateRound2Result = await runSignEd25519Round2( -// pool, -// TEMP_ENC_SECRET, -// round2Request, -// ); - -// expect(duplicateRound2Result.success).toBe(false); -// if (!duplicateRound2Result.success) { -// expect(duplicateRound2Result.code).toBe("INVALID_TSS_SESSION"); -// } -// }); - -// it("should fail when using COMPLETED session for Round2", async () => { -// const { walletId, customerId, clientKeygenOutput } = -// await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message for signing"); - -// // Complete full signing flow first -// const round1Res = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, { -// email: TEST_EMAIL, -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...testMessage], -// }); -// expect(round1Res.success).toBe(true); -// if (!round1Res.success) throw new Error("Round 1 failed"); - -// const clientR1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); - -// const allCommitments = [ -// { identifier: clientR1.identifier, commitments: clientR1.commitments }, -// { -// identifier: round1Res.data.commitments_0.identifier, -// commitments: round1Res.data.commitments_0.commitments, -// }, -// ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - -// const round2Res = await runSignEd25519Round2(pool, TEMP_ENC_SECRET, { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: round1Res.data.session_id, -// commitments_1: { -// identifier: clientR1.identifier, -// commitments: clientR1.commitments, -// }, -// }); -// expect(round2Res.success).toBe(true); -// if (!round2Res.success) throw new Error("Round 2 failed"); - -// const clientR2 = clientRunSignRound2Ed25519( -// testMessage, -// new Uint8Array(clientKeygenOutput.key_package), -// new Uint8Array(clientR1.nonces), -// allCommitments, -// ); - -// const allShares = [ -// { -// identifier: clientR2.identifier, -// signature_share: clientR2.signature_share, -// }, -// { -// identifier: round2Res.data.signature_share_0.identifier, -// signature_share: round2Res.data.signature_share_0.signature_share, -// }, -// ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - -// // Complete the signing with Aggregate -// const aggRes = await runSignEd25519Aggregate(pool, TEMP_ENC_SECRET, { -// email: TEST_EMAIL, -// wallet_id: walletId, -// msg: [...testMessage], -// all_commitments: allCommitments, -// all_signature_shares: allShares, -// }); -// expect(aggRes.success).toBe(true); - -// // Now try to use the COMPLETED session for Round2 - should fail -// const newClientR1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); -// const replayRound2Res = await runSignEd25519Round2( -// pool, -// TEMP_ENC_SECRET, -// { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: round1Res.data.session_id, // Reusing completed session -// commitments_1: { -// identifier: newClientR1.identifier, -// commitments: newClientR1.commitments, -// }, -// }, -// ); - -// expect(replayRound2Res.success).toBe(false); -// if (!replayRound2Res.success) { -// expect(replayRound2Res.code).toBe("INVALID_TSS_SESSION"); -// } -// }); -// }); - -// describe("runSignEd25519Aggregate", () => { -// it("should aggregate signatures and produce valid signature", async () => { -// const { walletId, customerId, clientKeygenOutput, serverKeygenOutput } = -// await setUpEd25519Wallet(pool); -// const testMessage = new TextEncoder().encode("Test message for signing"); - -// // Round 1: Both parties generate commitments -// const round1Request: SignEd25519Round1Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...testMessage], -// }; -// const serverRound1Result = await runSignEd25519Round1( -// pool, -// TEMP_ENC_SECRET, -// round1Request, -// ); -// expect(serverRound1Result.success).toBe(true); -// if (!serverRound1Result.success) throw new Error("Server Round 1 failed"); - -// const clientRound1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); - -// // Collect all commitments (sorted by identifier) -// const allCommitments = [ -// { -// identifier: clientRound1.identifier, -// commitments: clientRound1.commitments, -// }, -// { -// identifier: serverRound1Result.data.commitments_0.identifier, -// commitments: serverRound1Result.data.commitments_0.commitments, -// }, -// ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - -// // Round 2: Both parties generate signature shares -// const round2Request: SignEd25519Round2Request = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: serverRound1Result.data.session_id, -// commitments_1: { -// identifier: clientRound1.identifier, -// commitments: clientRound1.commitments, -// }, -// }; -// const serverRound2Result = await runSignEd25519Round2( -// pool, -// TEMP_ENC_SECRET, -// round2Request, -// ); -// expect(serverRound2Result.success).toBe(true); -// if (!serverRound2Result.success) throw new Error("Server Round 2 failed"); - -// const clientRound2 = clientRunSignRound2Ed25519( -// testMessage, -// new Uint8Array(clientKeygenOutput.key_package), -// new Uint8Array(clientRound1.nonces), -// allCommitments, -// ); - -// // Collect all signature shares (sorted by identifier) -// const allSignatureShares = [ -// { -// identifier: clientRound2.identifier, -// signature_share: clientRound2.signature_share, -// }, -// { -// identifier: serverRound2Result.data.signature_share_0.identifier, -// signature_share: -// serverRound2Result.data.signature_share_0.signature_share, -// }, -// ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - -// // Aggregate -// const aggregateRequest: SignEd25519AggregateRequest = { -// email: TEST_EMAIL, -// wallet_id: walletId, -// msg: [...testMessage], -// all_commitments: allCommitments, -// all_signature_shares: allSignatureShares, -// }; -// const aggregateResult = await runSignEd25519Aggregate( -// pool, -// TEMP_ENC_SECRET, -// aggregateRequest, -// ); - -// expect(aggregateResult.success).toBe(true); -// if (aggregateResult.success) { -// expect(aggregateResult.data.signature).toBeDefined(); -// expect(aggregateResult.data.signature.length).toBe(64); - -// // Verify the signature -// const isValid = runVerifyEd25519( -// testMessage, -// new Uint8Array(aggregateResult.data.signature), -// new Uint8Array(clientKeygenOutput.public_key_package), -// ); -// expect(isValid).toBe(true); -// } -// }); - -// it("should fail with wrong wallet type", async () => { -// // Create a secp256k1 wallet instead of ed25519 -// await setUpKSNodes(pool); -// await insertKeyShareNodeMeta(pool, { sss_threshold: SSS_THRESHOLD }); - -// const createUserRes = await createUser(pool, TEST_EMAIL, "google"); -// if (!createUserRes.success) throw new Error("Failed to create user"); - -// const encryptedShare = await encryptDataAsync( -// JSON.stringify({ private_share: "test", public_key: "test" }), -// TEMP_ENC_SECRET, -// ); - -// const createWalletRes = await createWallet(pool, { -// user_id: createUserRes.data.user_id, -// curve_type: "secp256k1", -// public_key: Buffer.from("03" + "00".repeat(32), "hex"), -// enc_tss_share: Buffer.from(encryptedShare, "utf-8"), -// sss_threshold: SSS_THRESHOLD, -// status: "ACTIVE" as WalletStatus, -// }); -// if (!createWalletRes.success) throw new Error("Failed to create wallet"); - -// const aggregateRequest: SignEd25519AggregateRequest = { -// email: TEST_EMAIL, -// wallet_id: createWalletRes.data.wallet_id, -// msg: [1, 2, 3], -// all_commitments: [], -// all_signature_shares: [], -// }; - -// const result = await runSignEd25519Aggregate( -// pool, -// TEMP_ENC_SECRET, -// aggregateRequest, -// ); - -// expect(result.success).toBe(false); -// if (!result.success) { -// expect(result.code).toBe("INVALID_WALLET_TYPE"); -// } -// }); - -// it("should fail with invalid wallet_id", async () => { -// await setUpEd25519Wallet(pool); - -// const aggregateRequest: SignEd25519AggregateRequest = { -// email: TEST_EMAIL, -// wallet_id: "00000000-0000-0000-0000-000000000000", -// msg: [1, 2, 3], -// all_commitments: [], -// all_signature_shares: [], -// }; - -// const result = await runSignEd25519Aggregate( -// pool, -// TEMP_ENC_SECRET, -// aggregateRequest, -// ); - -// expect(result.success).toBe(false); -// if (!result.success) { -// expect(result.code).toBe("UNAUTHORIZED"); -// } -// }); -// }); - -// describe("Full signing flow", () => { -// it("should complete full signing flow with valid signature verification", async () => { -// const { walletId, customerId, clientKeygenOutput } = -// await setUpEd25519Wallet(pool); -// const messages = [ -// "Hello, Solana!", -// "Transaction data", -// "Another message to sign", -// ]; - -// for (const msgStr of messages) { -// const message = new TextEncoder().encode(msgStr); - -// // Round 1 -// const round1Res = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, { -// email: TEST_EMAIL, -// wallet_id: walletId, -// customer_id: customerId, -// msg: [...message], -// }); -// expect(round1Res.success).toBe(true); -// if (!round1Res.success) continue; - -// const clientR1 = clientRunSignRound1Ed25519( -// new Uint8Array(clientKeygenOutput.key_package), -// ); - -// const allCommitments = [ -// { -// identifier: clientR1.identifier, -// commitments: clientR1.commitments, -// }, -// { -// identifier: round1Res.data.commitments_0.identifier, -// commitments: round1Res.data.commitments_0.commitments, -// }, -// ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - -// // Round 2 -// const round2Res = await runSignEd25519Round2(pool, TEMP_ENC_SECRET, { -// email: TEST_EMAIL, -// wallet_id: walletId, -// session_id: round1Res.data.session_id, -// commitments_1: { -// identifier: clientR1.identifier, -// commitments: clientR1.commitments, -// }, -// }); -// expect(round2Res.success).toBe(true); -// if (!round2Res.success) continue; - -// const clientR2 = clientRunSignRound2Ed25519( -// message, -// new Uint8Array(clientKeygenOutput.key_package), -// new Uint8Array(clientR1.nonces), -// allCommitments, -// ); - -// const allShares = [ -// { -// identifier: clientR2.identifier, -// signature_share: clientR2.signature_share, -// }, -// { -// identifier: round2Res.data.signature_share_0.identifier, -// signature_share: round2Res.data.signature_share_0.signature_share, -// }, -// ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - -// // Aggregate -// const aggRes = await runSignEd25519Aggregate(pool, TEMP_ENC_SECRET, { -// email: TEST_EMAIL, -// wallet_id: walletId, -// msg: [...message], -// all_commitments: allCommitments, -// all_signature_shares: allShares, -// }); -// expect(aggRes.success).toBe(true); -// if (!aggRes.success) continue; - -// // Verify -// const isValid = runVerifyEd25519( -// message, -// new Uint8Array(aggRes.data.signature), -// new Uint8Array(clientKeygenOutput.public_key_package), -// ); -// expect(isValid).toBe(true); -// } -// }); -// }); -// }); +import { Pool } from "pg"; +import type { + SignEd25519Round1Request, + SignEd25519Round2Request, + SignEd25519AggregateRequest, +} from "@oko-wallet/oko-types/tss"; +import { + TssStageType, + SignEd25519StageStatus, + TssSessionState, +} from "@oko-wallet/oko-types/tss"; +import { getTssStageWithSessionData } from "@oko-wallet/oko-pg-interface/tss"; +import { Participant } from "@oko-wallet/teddsa-interface"; +import { + runKeygenCentralizedEd25519, + runSignRound1Ed25519 as clientRunSignRound1Ed25519, + runSignRound2Ed25519 as clientRunSignRound2Ed25519, + runVerifyEd25519, + extractKeyPackageSharesEd25519, +} from "@oko-wallet/teddsa-addon/src/server"; +import { createPgConn } from "@oko-wallet/postgres-lib"; +import type { WalletStatus } from "@oko-wallet/oko-types/wallets"; +import { insertKSNode } from "@oko-wallet/oko-pg-interface/ks_nodes"; +import { createWallet } from "@oko-wallet/oko-pg-interface/oko_wallets"; +import { createUser } from "@oko-wallet/oko-pg-interface/oko_users"; +import { insertKeyShareNodeMeta } from "@oko-wallet/oko-pg-interface/key_share_node_meta"; +import { insertCustomer } from "@oko-wallet/oko-pg-interface/customers"; +import { encryptDataAsync } from "@oko-wallet/crypto-js/node"; + +import { resetPgDatabase } from "@oko-wallet-tss-api/testing/database"; +import { testPgConfig } from "@oko-wallet-tss-api/database/test_config"; +import { + runSignEd25519Round1, + runSignEd25519Round2, + runSignEd25519Aggregate, +} from "@oko-wallet-tss-api/api/sign_ed25519"; +import { TEMP_ENC_SECRET } from "@oko-wallet-tss-api/api/utils"; + +const SSS_THRESHOLD = 2; +const TEST_EMAIL = "test-ed25519@test.com"; + +interface TestSetupResult { + pool: Pool; + walletId: string; + customerId: string; + clientKeygenOutput: ReturnType< + typeof runKeygenCentralizedEd25519 + >["keygen_outputs"][Participant.P0]; + serverKeygenOutput: ReturnType< + typeof runKeygenCentralizedEd25519 + >["keygen_outputs"][Participant.P1]; +} + +async function setUpKSNodes(pool: Pool): Promise { + const ksNodeNames = ["ksNode1", "ksNode2"]; + const ksNodeIds = []; + const createKSNodesRes = await Promise.all( + ksNodeNames.map((ksNodeName) => + insertKSNode(pool, ksNodeName, `http://test.com/${ksNodeName}`), + ), + ); + for (const res of createKSNodesRes) { + if (res.success === false) { + throw new Error("Failed to create ks nodes"); + } + ksNodeIds.push(res.data.node_id); + } + return ksNodeIds; +} + +async function setUpEd25519Wallet(pool: Pool): Promise { + // Generate keys using centralized keygen + const keygenResult = runKeygenCentralizedEd25519(); + const clientKeygenOutput = keygenResult.keygen_outputs[Participant.P0]; + const serverKeygenOutput = keygenResult.keygen_outputs[Participant.P1]; + + // Set up KS nodes and metadata + const ksNodeIds = await setUpKSNodes(pool); + await insertKeyShareNodeMeta(pool, { + sss_threshold: SSS_THRESHOLD, + }); + + // Create customer + const customerId = crypto.randomUUID(); + const insertCustomerRes = await insertCustomer(pool, { + customer_id: customerId, + label: "test-customer", + status: "ACTIVE", + url: null, + logo_url: null, + theme: "dark", + }); + if (insertCustomerRes.success === false) { + throw new Error(`Failed to create customer: ${insertCustomerRes.err}`); + } + + // Create user + const createUserRes = await createUser(pool, TEST_EMAIL, "google"); + if (createUserRes.success === false) { + throw new Error(`Failed to create user: ${createUserRes.err}`); + } + const userId = createUserRes.data.user_id; + + // Extract signing_share and verifying_share from server key_package + const serverKeyPackageShares = extractKeyPackageSharesEd25519( + new Uint8Array(serverKeygenOutput.key_package), + ); + + // Store only signing_share and verifying_share (64 bytes total) + const sharesData = { + signing_share: serverKeyPackageShares.signing_share, + verifying_share: serverKeyPackageShares.verifying_share, + }; + + const encryptedShare = await encryptDataAsync( + JSON.stringify(sharesData), + TEMP_ENC_SECRET, + ); + + // Create Ed25519 wallet + const createWalletRes = await createWallet(pool, { + user_id: userId, + curve_type: "ed25519", + public_key: Buffer.from(keygenResult.public_key), + enc_tss_share: Buffer.from(encryptedShare, "utf-8"), + sss_threshold: SSS_THRESHOLD, + status: "ACTIVE" as WalletStatus, + }); + if (createWalletRes.success === false) { + throw new Error(`Failed to create wallet: ${createWalletRes.err}`); + } + const walletId = createWalletRes.data.wallet_id; + + return { + pool, + walletId, + customerId, + clientKeygenOutput, + serverKeygenOutput, + }; +} + +describe("Ed25519 Signing", () => { + let pool: Pool; + + beforeAll(async () => { + const config = testPgConfig; + const createPostgresRes = await createPgConn({ + database: config.database, + host: config.host, + password: config.password, + user: config.user, + port: config.port, + ssl: config.ssl, + }); + + if (createPostgresRes.success === false) { + console.error(createPostgresRes.err); + throw new Error("Failed to create postgres database"); + } + + pool = createPostgresRes.data; + }); + + afterAll(async () => { + await pool.end(); + }); + + beforeEach(async () => { + await resetPgDatabase(pool); + }); + + describe("runSignEd25519Round1", () => { + it("should generate commitments successfully", async () => { + const { walletId, customerId } = await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message"); + + const request: SignEd25519Round1Request = { + email: TEST_EMAIL, + wallet_id: walletId, + customer_id: customerId, + msg: [...testMessage], + }; + + const result = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, request); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.session_id).toBeDefined(); + expect(result.data.commitments_0).toBeDefined(); + expect(result.data.commitments_0.identifier).toBeDefined(); + expect(result.data.commitments_0.commitments).toBeDefined(); + expect(Array.isArray(result.data.commitments_0.identifier)).toBe(true); + expect(Array.isArray(result.data.commitments_0.commitments)).toBe(true); + + // Verify stage status is ROUND_1 + const getStageRes = await getTssStageWithSessionData( + pool, + result.data.session_id, + TssStageType.SIGN_ED25519, + ); + expect(getStageRes.success).toBe(true); + if (getStageRes.success && getStageRes.data) { + expect(getStageRes.data.stage_status).toBe( + SignEd25519StageStatus.ROUND_1, + ); + expect(getStageRes.data.session_state).toBe( + TssSessionState.IN_PROGRESS, + ); + } + } + }); + + it("should fail with invalid email", async () => { + const { walletId, customerId } = await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message"); + + const request: SignEd25519Round1Request = { + email: "wrong@test.com", + wallet_id: walletId, + customer_id: customerId, + msg: [...testMessage], + }; + + const result = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, request); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.code).toBe("UNAUTHORIZED"); + } + }); + + it("should fail with invalid wallet_id", async () => { + const { customerId } = await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message"); + + const request: SignEd25519Round1Request = { + email: TEST_EMAIL, + wallet_id: "00000000-0000-0000-0000-000000000000", + customer_id: customerId, + msg: [...testMessage], + }; + + const result = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, request); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.code).toBe("UNAUTHORIZED"); + } + }); + }); + + describe("runSignEd25519Round2", () => { + it("should generate signature share successfully", async () => { + const { walletId, customerId, clientKeygenOutput } = + await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message for Ed25519"); + + // Round 1: Get server commitments + const round1Request: SignEd25519Round1Request = { + email: TEST_EMAIL, + wallet_id: walletId, + customer_id: customerId, + msg: [...testMessage], + }; + const round1Result = await runSignEd25519Round1( + pool, + TEMP_ENC_SECRET, + round1Request, + ); + expect(round1Result.success).toBe(true); + if (!round1Result.success) throw new Error("Round 1 failed"); + + // Client generates their round 1 output + const clientRound1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + // Round 2: Get server signature share + const round2Request: SignEd25519Round2Request = { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Result.data.session_id, + commitments_1: { + identifier: clientRound1.identifier, + commitments: clientRound1.commitments, + }, + }; + const round2Result = await runSignEd25519Round2( + pool, + TEMP_ENC_SECRET, + round2Request, + ); + + expect(round2Result.success).toBe(true); + if (round2Result.success) { + expect(round2Result.data.signature_share_0).toBeDefined(); + expect(round2Result.data.signature_share_0.identifier).toBeDefined(); + expect( + round2Result.data.signature_share_0.signature_share, + ).toBeDefined(); + + // Verify stage status is ROUND_2 (not COMPLETED) + const getStageRes = await getTssStageWithSessionData( + pool, + round1Result.data.session_id, + TssStageType.SIGN_ED25519, + ); + expect(getStageRes.success).toBe(true); + if (getStageRes.success && getStageRes.data) { + expect(getStageRes.data.stage_status).toBe( + SignEd25519StageStatus.ROUND_2, + ); + expect(getStageRes.data.session_state).toBe( + TssSessionState.IN_PROGRESS, + ); + } + } + }); + + it("should fail with invalid session_id", async () => { + const { walletId, customerId, clientKeygenOutput } = + await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message"); + + const clientRound1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + const round2Request: SignEd25519Round2Request = { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: "00000000-0000-0000-0000-000000000000", + commitments_1: { + identifier: clientRound1.identifier, + commitments: clientRound1.commitments, + }, + }; + const result = await runSignEd25519Round2( + pool, + TEMP_ENC_SECRET, + round2Request, + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.code).toBe("INVALID_TSS_SESSION"); + } + }); + + it("should fail when Round2 is called twice (duplicate call prevention)", async () => { + const { walletId, customerId, clientKeygenOutput } = + await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message"); + + // Round 1: Get server commitments + const round1Request: SignEd25519Round1Request = { + email: TEST_EMAIL, + wallet_id: walletId, + customer_id: customerId, + msg: [...testMessage], + }; + const round1Result = await runSignEd25519Round1( + pool, + TEMP_ENC_SECRET, + round1Request, + ); + expect(round1Result.success).toBe(true); + if (!round1Result.success) throw new Error("Round 1 failed"); + + const clientRound1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + // First Round 2 call - should succeed + const round2Request: SignEd25519Round2Request = { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Result.data.session_id, + commitments_1: { + identifier: clientRound1.identifier, + commitments: clientRound1.commitments, + }, + }; + const round2Result = await runSignEd25519Round2( + pool, + TEMP_ENC_SECRET, + round2Request, + ); + expect(round2Result.success).toBe(true); + + // Verify stage status is ROUND_2 after first Round2 call + const getStageRes = await getTssStageWithSessionData( + pool, + round1Result.data.session_id, + TssStageType.SIGN_ED25519, + ); + expect(getStageRes.success).toBe(true); + if (getStageRes.success && getStageRes.data) { + expect(getStageRes.data.stage_status).toBe( + SignEd25519StageStatus.ROUND_2, + ); + } + + // Second Round 2 call with same session - should fail (already ROUND_2) + const duplicateRound2Result = await runSignEd25519Round2( + pool, + TEMP_ENC_SECRET, + round2Request, + ); + + expect(duplicateRound2Result.success).toBe(false); + if (!duplicateRound2Result.success) { + expect(duplicateRound2Result.code).toBe("INVALID_TSS_SESSION"); + } + }); + + it("should fail when using COMPLETED session for Round2", async () => { + const { walletId, customerId, clientKeygenOutput } = + await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message for signing"); + + // Complete full signing flow first + const round1Res = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, { + email: TEST_EMAIL, + wallet_id: walletId, + customer_id: customerId, + msg: [...testMessage], + }); + expect(round1Res.success).toBe(true); + if (!round1Res.success) throw new Error("Round 1 failed"); + + const clientR1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + const allCommitments = [ + { identifier: clientR1.identifier, commitments: clientR1.commitments }, + { + identifier: round1Res.data.commitments_0.identifier, + commitments: round1Res.data.commitments_0.commitments, + }, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + const round2Res = await runSignEd25519Round2(pool, TEMP_ENC_SECRET, { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Res.data.session_id, + commitments_1: { + identifier: clientR1.identifier, + commitments: clientR1.commitments, + }, + }); + expect(round2Res.success).toBe(true); + if (!round2Res.success) throw new Error("Round 2 failed"); + + const clientR2 = clientRunSignRound2Ed25519( + testMessage, + new Uint8Array(clientKeygenOutput.key_package), + new Uint8Array(clientR1.nonces), + allCommitments, + ); + + const allShares = [ + { + identifier: clientR2.identifier, + signature_share: clientR2.signature_share, + }, + { + identifier: round2Res.data.signature_share_0.identifier, + signature_share: round2Res.data.signature_share_0.signature_share, + }, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Extract user's verifying_share for aggregate + const userKeyPackageShares = extractKeyPackageSharesEd25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + // Verify stage status is ROUND_2 before aggregate + const getStageBeforeAggRes = await getTssStageWithSessionData( + pool, + round1Res.data.session_id, + TssStageType.SIGN_ED25519, + ); + expect(getStageBeforeAggRes.success).toBe(true); + if (getStageBeforeAggRes.success && getStageBeforeAggRes.data) { + expect(getStageBeforeAggRes.data.stage_status).toBe( + SignEd25519StageStatus.ROUND_2, + ); + } + + // Complete the signing with Aggregate + const aggRes = await runSignEd25519Aggregate(pool, TEMP_ENC_SECRET, { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Res.data.session_id, + msg: [...testMessage], + all_commitments: allCommitments, + all_signature_shares: allShares, + user_verifying_share: userKeyPackageShares.verifying_share, + }); + expect(aggRes.success).toBe(true); + + // Verify stage status is COMPLETED after aggregate + const getStageAfterAggRes = await getTssStageWithSessionData( + pool, + round1Res.data.session_id, + TssStageType.SIGN_ED25519, + ); + expect(getStageAfterAggRes.success).toBe(true); + if (getStageAfterAggRes.success && getStageAfterAggRes.data) { + expect(getStageAfterAggRes.data.stage_status).toBe( + SignEd25519StageStatus.COMPLETED, + ); + expect(getStageAfterAggRes.data.session_state).toBe( + TssSessionState.COMPLETED, + ); + } + + // Now try to use the COMPLETED session for Round2 - should fail + const newClientR1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + const replayRound2Res = await runSignEd25519Round2( + pool, + TEMP_ENC_SECRET, + { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Res.data.session_id, // Reusing completed session + commitments_1: { + identifier: newClientR1.identifier, + commitments: newClientR1.commitments, + }, + }, + ); + + expect(replayRound2Res.success).toBe(false); + if (!replayRound2Res.success) { + expect(replayRound2Res.code).toBe("INVALID_TSS_SESSION"); + } + }); + }); + + describe("runSignEd25519Aggregate", () => { + it("should aggregate signatures and produce valid signature", async () => { + const { walletId, customerId, clientKeygenOutput, serverKeygenOutput } = + await setUpEd25519Wallet(pool); + const testMessage = new TextEncoder().encode("Test message for signing"); + + // Round 1: Both parties generate commitments + const round1Request: SignEd25519Round1Request = { + email: TEST_EMAIL, + wallet_id: walletId, + customer_id: customerId, + msg: [...testMessage], + }; + const serverRound1Result = await runSignEd25519Round1( + pool, + TEMP_ENC_SECRET, + round1Request, + ); + expect(serverRound1Result.success).toBe(true); + if (!serverRound1Result.success) throw new Error("Server Round 1 failed"); + + const clientRound1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + // Collect all commitments (sorted by identifier) + const allCommitments = [ + { + identifier: clientRound1.identifier, + commitments: clientRound1.commitments, + }, + { + identifier: serverRound1Result.data.commitments_0.identifier, + commitments: serverRound1Result.data.commitments_0.commitments, + }, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Round 2: Both parties generate signature shares + const round2Request: SignEd25519Round2Request = { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: serverRound1Result.data.session_id, + commitments_1: { + identifier: clientRound1.identifier, + commitments: clientRound1.commitments, + }, + }; + const serverRound2Result = await runSignEd25519Round2( + pool, + TEMP_ENC_SECRET, + round2Request, + ); + expect(serverRound2Result.success).toBe(true); + if (!serverRound2Result.success) throw new Error("Server Round 2 failed"); + + const clientRound2 = clientRunSignRound2Ed25519( + testMessage, + new Uint8Array(clientKeygenOutput.key_package), + new Uint8Array(clientRound1.nonces), + allCommitments, + ); + + // Collect all signature shares (sorted by identifier) + const allSignatureShares = [ + { + identifier: clientRound2.identifier, + signature_share: clientRound2.signature_share, + }, + { + identifier: serverRound2Result.data.signature_share_0.identifier, + signature_share: + serverRound2Result.data.signature_share_0.signature_share, + }, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Extract user's verifying_share for aggregate + const userKeyPackageShares = extractKeyPackageSharesEd25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + // Aggregate + const aggregateRequest: SignEd25519AggregateRequest = { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: serverRound1Result.data.session_id, + msg: [...testMessage], + all_commitments: allCommitments, + all_signature_shares: allSignatureShares, + user_verifying_share: userKeyPackageShares.verifying_share, + }; + const aggregateResult = await runSignEd25519Aggregate( + pool, + TEMP_ENC_SECRET, + aggregateRequest, + ); + + expect(aggregateResult.success).toBe(true); + if (aggregateResult.success) { + expect(aggregateResult.data.signature).toBeDefined(); + expect(aggregateResult.data.signature.length).toBe(64); + + // Verify stage status is COMPLETED after aggregate + const getStageRes = await getTssStageWithSessionData( + pool, + serverRound1Result.data.session_id, + TssStageType.SIGN_ED25519, + ); + expect(getStageRes.success).toBe(true); + if (getStageRes.success && getStageRes.data) { + expect(getStageRes.data.stage_status).toBe( + SignEd25519StageStatus.COMPLETED, + ); + expect(getStageRes.data.session_state).toBe( + TssSessionState.COMPLETED, + ); + } + + // Verify the signature + const isValid = runVerifyEd25519( + testMessage, + new Uint8Array(aggregateResult.data.signature), + new Uint8Array(clientKeygenOutput.public_key_package), + ); + expect(isValid).toBe(true); + } + }); + + it("should fail with wrong wallet type", async () => { + // Create a secp256k1 wallet instead of ed25519 + await setUpKSNodes(pool); + await insertKeyShareNodeMeta(pool, { sss_threshold: SSS_THRESHOLD }); + + const createUserRes = await createUser(pool, TEST_EMAIL, "google"); + if (!createUserRes.success) throw new Error("Failed to create user"); + + const encryptedShare = await encryptDataAsync( + JSON.stringify({ private_share: "test", public_key: "test" }), + TEMP_ENC_SECRET, + ); + + const createWalletRes = await createWallet(pool, { + user_id: createUserRes.data.user_id, + curve_type: "secp256k1", + public_key: Buffer.from("03" + "00".repeat(32), "hex"), + enc_tss_share: Buffer.from(encryptedShare, "utf-8"), + sss_threshold: SSS_THRESHOLD, + status: "ACTIVE" as WalletStatus, + }); + if (!createWalletRes.success) throw new Error("Failed to create wallet"); + + const aggregateRequest: SignEd25519AggregateRequest = { + email: TEST_EMAIL, + wallet_id: createWalletRes.data.wallet_id, + session_id: "00000000-0000-0000-0000-000000000000", // Dummy session_id for error test + msg: [1, 2, 3], + all_commitments: [], + all_signature_shares: [], + user_verifying_share: new Array(32).fill(0), // Dummy value for error test + }; + + const result = await runSignEd25519Aggregate( + pool, + TEMP_ENC_SECRET, + aggregateRequest, + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.code).toBe("INVALID_WALLET_TYPE"); + } + }); + + it("should fail with invalid wallet_id", async () => { + await setUpEd25519Wallet(pool); + + const aggregateRequest: SignEd25519AggregateRequest = { + email: TEST_EMAIL, + wallet_id: "00000000-0000-0000-0000-000000000000", + session_id: "00000000-0000-0000-0000-000000000000", // Dummy session_id for error test + msg: [1, 2, 3], + all_commitments: [], + all_signature_shares: [], + user_verifying_share: new Array(32).fill(0), // Dummy value for error test + }; + + const result = await runSignEd25519Aggregate( + pool, + TEMP_ENC_SECRET, + aggregateRequest, + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.code).toBe("UNAUTHORIZED"); + } + }); + }); + + describe("Full signing flow", () => { + it("should complete full signing flow with valid signature verification", async () => { + const { walletId, customerId, clientKeygenOutput } = + await setUpEd25519Wallet(pool); + const messages = [ + "Hello, Solana!", + "Transaction data", + "Another message to sign", + ]; + + for (const msgStr of messages) { + const message = new TextEncoder().encode(msgStr); + + // Round 1 + const round1Res = await runSignEd25519Round1(pool, TEMP_ENC_SECRET, { + email: TEST_EMAIL, + wallet_id: walletId, + customer_id: customerId, + msg: [...message], + }); + expect(round1Res.success).toBe(true); + if (!round1Res.success) continue; + + const clientR1 = clientRunSignRound1Ed25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + const allCommitments = [ + { + identifier: clientR1.identifier, + commitments: clientR1.commitments, + }, + { + identifier: round1Res.data.commitments_0.identifier, + commitments: round1Res.data.commitments_0.commitments, + }, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Round 2 + const round2Res = await runSignEd25519Round2(pool, TEMP_ENC_SECRET, { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Res.data.session_id, + commitments_1: { + identifier: clientR1.identifier, + commitments: clientR1.commitments, + }, + }); + expect(round2Res.success).toBe(true); + if (!round2Res.success) continue; + + const clientR2 = clientRunSignRound2Ed25519( + message, + new Uint8Array(clientKeygenOutput.key_package), + new Uint8Array(clientR1.nonces), + allCommitments, + ); + + const allShares = [ + { + identifier: clientR2.identifier, + signature_share: clientR2.signature_share, + }, + { + identifier: round2Res.data.signature_share_0.identifier, + signature_share: round2Res.data.signature_share_0.signature_share, + }, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Extract user's verifying_share for aggregate + const userKeyPackageShares = extractKeyPackageSharesEd25519( + new Uint8Array(clientKeygenOutput.key_package), + ); + + // Verify stage status is ROUND_2 before aggregate + const getStageBeforeAggRes = await getTssStageWithSessionData( + pool, + round1Res.data.session_id, + TssStageType.SIGN_ED25519, + ); + if (getStageBeforeAggRes.success && getStageBeforeAggRes.data) { + expect(getStageBeforeAggRes.data.stage_status).toBe( + SignEd25519StageStatus.ROUND_2, + ); + } + + // Aggregate + const aggRes = await runSignEd25519Aggregate(pool, TEMP_ENC_SECRET, { + email: TEST_EMAIL, + wallet_id: walletId, + session_id: round1Res.data.session_id, + msg: [...message], + all_commitments: allCommitments, + all_signature_shares: allShares, + user_verifying_share: userKeyPackageShares.verifying_share, + }); + expect(aggRes.success).toBe(true); + if (!aggRes.success) continue; + + // Verify stage status is COMPLETED after aggregate + const getStageAfterAggRes = await getTssStageWithSessionData( + pool, + round1Res.data.session_id, + TssStageType.SIGN_ED25519, + ); + if (getStageAfterAggRes.success && getStageAfterAggRes.data) { + expect(getStageAfterAggRes.data.stage_status).toBe( + SignEd25519StageStatus.COMPLETED, + ); + expect(getStageAfterAggRes.data.session_state).toBe( + TssSessionState.COMPLETED, + ); + } + + // Verify + const isValid = runVerifyEd25519( + message, + new Uint8Array(aggRes.data.signature), + new Uint8Array(clientKeygenOutput.public_key_package), + ); + expect(isValid).toBe(true); + } + }); + }); +}); diff --git a/backend/tss_api/src/api/sign_ed25519/index.ts b/backend/tss_api/src/api/sign_ed25519/index.ts index 38c9eed68..2e33a2704 100644 --- a/backend/tss_api/src/api/sign_ed25519/index.ts +++ b/backend/tss_api/src/api/sign_ed25519/index.ts @@ -11,17 +11,12 @@ import type { SignEd25519AggregateRequest, SignEd25519AggregateResponse, SignEd25519StageData, - SignEd25519Request, - SignEd25519Response, - PresignEd25519StageData, } from "@oko-wallet/oko-types/tss"; import { TssStageType, SignEd25519StageStatus, - PresignEd25519StageStatus, TssSessionState, } from "@oko-wallet/oko-types/tss"; -import type { KeygenEd25519Output } from "@oko-wallet/oko-types/tss"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import { Pool } from "pg"; import { decryptDataAsync } from "@oko-wallet/crypto-js/node"; @@ -29,7 +24,13 @@ import { runSignRound1Ed25519, runSignRound2Ed25519, runAggregateEd25519, + reconstructKeyPackageEd25519, + reconstructPublicKeyPackageEd25519, } from "@oko-wallet/teddsa-addon/src/server"; +import { + Participant, + participantToIdentifier, +} from "@oko-wallet/teddsa-interface"; import { validateWalletEmail, @@ -73,11 +74,34 @@ export async function runSignEd25519Round1( encryptedShare, encryptionSecret, ); - const keygenOutput: KeygenEd25519Output = JSON.parse(decryptedShare); + const storedShares = JSON.parse(decryptedShare) as { + signing_share: number[]; + verifying_share: number[]; + }; - const round1Result = runSignRound1Ed25519( - new Uint8Array(keygenOutput.key_package), - ); + // Reconstruct key_package from stored shares + const serverIdentifier = participantToIdentifier(Participant.P1); + const verifyingKey = Array.from(wallet.public_key); + const minSigners = 2; + + let keyPackageBytes: Uint8Array; + try { + keyPackageBytes = reconstructKeyPackageEd25519( + new Uint8Array(storedShares.signing_share), + new Uint8Array(storedShares.verifying_share), + new Uint8Array(serverIdentifier), + new Uint8Array(verifyingKey), + minSigners, + ); + } catch (error) { + return { + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to reconstruct key_package: ${error instanceof Error ? error.message : String(error)}`, + }; + } + + const round1Result = runSignRound1Ed25519(keyPackageBytes); // Create TSS session const sessionRes = await createTssSession(db, { @@ -219,7 +243,32 @@ export async function runSignEd25519Round2( encryptedShare, encryptionSecret, ); - const keygenOutput: KeygenEd25519Output = JSON.parse(decryptedShare); + const storedShares = JSON.parse(decryptedShare) as { + signing_share: number[]; + verifying_share: number[]; + }; + + // Reconstruct key_package from stored shares + const serverIdentifier = participantToIdentifier(Participant.P1); + const verifyingKey = Array.from(wallet.public_key); + const minSigners = 2; + + let keyPackageBytes: Uint8Array; + try { + keyPackageBytes = reconstructKeyPackageEd25519( + new Uint8Array(storedShares.signing_share), + new Uint8Array(storedShares.verifying_share), + new Uint8Array(serverIdentifier), + new Uint8Array(verifyingKey), + minSigners, + ); + } catch (error) { + return { + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to reconstruct key_package: ${error instanceof Error ? error.message : String(error)}`, + }; + } const serverCommitment = { identifier, @@ -235,24 +284,23 @@ export async function runSignEd25519Round2( const round2Result = runSignRound2Ed25519( new Uint8Array(msg), - new Uint8Array(keygenOutput.key_package), + keyPackageBytes, new Uint8Array(nonces), allCommitments, ); - // Update stage and session atomically const updateRes = await updateTssStageWithSessionState( db, stage.stage_id, session_id, { - stage_status: SignEd25519StageStatus.COMPLETED, + stage_status: SignEd25519StageStatus.ROUND_2, stage_data: { ...stageData, signature_share: round2Result.signature_share, }, }, - TssSessionState.COMPLETED, + TssSessionState.IN_PROGRESS, ); if (!updateRes.success) { return { @@ -280,14 +328,21 @@ export async function runSignEd25519Round2( } } -// New presign-based sign function -export async function runSignEd25519( +export async function runSignEd25519Aggregate( db: Pool, encryptionSecret: string, - request: SignEd25519Request, -): Promise> { + request: SignEd25519AggregateRequest, +): Promise> { try { - const { email, wallet_id, session_id, msg, commitments_1 } = request; + const { + email, + wallet_id, + session_id, + msg, + all_commitments, + all_signature_shares, + user_verifying_share, + } = request; const validateWalletEmailRes = await validateWalletEmail( db, @@ -311,11 +366,44 @@ export async function runSignEd25519( }; } - // Get presign stage with session data + // Decrypt stored shares + const encryptedShare = wallet.enc_tss_share.toString("utf-8"); + const decryptedShare = await decryptDataAsync( + encryptedShare, + encryptionSecret, + ); + const storedShares = JSON.parse(decryptedShare) as { + signing_share: number[]; + verifying_share: number[]; + }; + + // Reconstruct public_key_package from user and server verifying_shares + const userIdentifier = participantToIdentifier(Participant.P0); + const serverIdentifier = participantToIdentifier(Participant.P1); + const verifyingKey = Array.from(wallet.public_key); + + let publicKeyPackageBytes: Uint8Array; + try { + publicKeyPackageBytes = reconstructPublicKeyPackageEd25519( + new Uint8Array(user_verifying_share), + new Uint8Array(userIdentifier), + new Uint8Array(storedShares.verifying_share), + new Uint8Array(serverIdentifier), + new Uint8Array(verifyingKey), + ); + } catch (error) { + return { + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to reconstruct public_key_package: ${error instanceof Error ? error.message : String(error)}`, + }; + } + + // Get stage with session data to update status after aggregate const getStageRes = await getTssStageWithSessionData( db, session_id, - TssStageType.PRESIGN_ED25519, + TssStageType.SIGN_ED25519, ); if (getStageRes.success === false) { return { @@ -335,60 +423,35 @@ export async function runSignEd25519( }; } - // Validate stage status (must be COMPLETED presign, not USED) - if (!validateTssStage(stage, PresignEd25519StageStatus.COMPLETED)) { + // Validate stage status (should be ROUND_2) + if (!validateTssStage(stage, SignEd25519StageStatus.ROUND_2)) { return { success: false, code: "INVALID_TSS_SESSION", - msg: "Presign not found or already used. Please call presign_ed25519 first.", + msg: "Round 2 state not found. Please call round2 first.", }; } - const stageData = stage.stage_data as PresignEd25519StageData; - const { nonces, identifier, commitments } = stageData; - - if (!nonces || !identifier || !commitments) { - return { - success: false, - code: "INVALID_TSS_SESSION", - msg: "Missing presign data in stage", - }; - } - - const encryptedShare = wallet.enc_tss_share.toString("utf-8"); - const decryptedShare = await decryptDataAsync( - encryptedShare, - encryptionSecret, - ); - const keygenOutput: KeygenEd25519Output = JSON.parse(decryptedShare); - - const serverCommitment = { - identifier, - commitments, - }; - - const allCommitments = [serverCommitment, commitments_1]; - allCommitments.sort((a, b) => { - const idA = a.identifier[0] ?? 0; - const idB = b.identifier[0] ?? 0; - return idA - idB; - }); - - const round2Result = runSignRound2Ed25519( + // Aggregate signature + const aggregateResult = runAggregateEd25519( new Uint8Array(msg), - new Uint8Array(keygenOutput.key_package), - new Uint8Array(nonces), - allCommitments, + all_commitments, + all_signature_shares, + publicKeyPackageBytes, ); - // Mark presign as USED and complete the session + // Update stage and session to COMPLETED after successful aggregate + const stageData = stage.stage_data as SignEd25519StageData; const updateRes = await updateTssStageWithSessionState( db, stage.stage_id, session_id, { - stage_status: PresignEd25519StageStatus.USED, - stage_data: stageData, + stage_status: SignEd25519StageStatus.COMPLETED, + stage_data: { + ...stageData, + signature: aggregateResult.signature, + }, }, TssSessionState.COMPLETED, ); @@ -400,69 +463,6 @@ export async function runSignEd25519( }; } - return { - success: true, - data: { - signature_share_0: { - identifier: round2Result.identifier, - signature_share: round2Result.signature_share, - }, - }, - }; - } catch (error) { - return { - success: false, - code: "UNKNOWN_ERROR", - msg: `runSignEd25519 error: ${error instanceof Error ? error.message : String(error)}`, - }; - } -} - -export async function runSignEd25519Aggregate( - db: Pool, - encryptionSecret: string, - request: SignEd25519AggregateRequest, -): Promise> { - try { - const { email, wallet_id, msg, all_commitments, all_signature_shares } = - request; - - const validateWalletEmailRes = await validateWalletEmail( - db, - wallet_id, - email, - ); - if (validateWalletEmailRes.success === false) { - return { - success: false, - code: "UNAUTHORIZED", - msg: validateWalletEmailRes.err, - }; - } - const wallet = validateWalletEmailRes.data; - - if (wallet.curve_type !== "ed25519") { - return { - success: false, - code: "INVALID_WALLET_TYPE", - msg: `Wallet is not ed25519 type: ${wallet.curve_type}`, - }; - } - - const encryptedShare = wallet.enc_tss_share.toString("utf-8"); - const decryptedShare = await decryptDataAsync( - encryptedShare, - encryptionSecret, - ); - const keygenOutput: KeygenEd25519Output = JSON.parse(decryptedShare); - - const aggregateResult = runAggregateEd25519( - new Uint8Array(msg), - all_commitments, - all_signature_shares, - new Uint8Array(keygenOutput.public_key_package), - ); - return { success: true, data: { diff --git a/backend/tss_api/src/api/wallet_ed25519/index.ts b/backend/tss_api/src/api/wallet_ed25519/index.ts index 107c2a360..30a21876e 100644 --- a/backend/tss_api/src/api/wallet_ed25519/index.ts +++ b/backend/tss_api/src/api/wallet_ed25519/index.ts @@ -8,10 +8,12 @@ import { Participant, participantToIdentifier, } from "@oko-wallet/teddsa-interface"; +import { reconstructPublicKeyPackageEd25519 } from "@oko-wallet/teddsa-addon/src/server"; export interface WalletEd25519PublicInfoRequest { user_identifier: string; auth_type: AuthType; + user_verifying_share: number[]; // P0's verifying_share (32 bytes) } export interface WalletEd25519PublicInfoResponse { @@ -31,7 +33,7 @@ export async function getWalletEd25519PublicInfo( request: WalletEd25519PublicInfoRequest, ): Promise> { try { - const { user_identifier, auth_type } = request; + const { user_identifier, auth_type, user_verifying_share } = request; // Get user const getUserRes = await getUserByEmailAndAuthType( @@ -77,25 +79,44 @@ export async function getWalletEd25519PublicInfo( } const wallet = getWalletRes.data; - // Decrypt the stored key package data + // Decrypt stored shares const encryptedShare = wallet.enc_tss_share.toString("utf-8"); const decryptedShare = await decryptDataAsync( encryptedShare, encryptionSecret, ); - const keyPackageData = JSON.parse(decryptedShare) as { - key_package: number[]; - public_key_package: number[]; - identifier: number[]; + const storedShares = JSON.parse(decryptedShare) as { + signing_share: number[]; + verifying_share: number[]; }; - // Return public info for client key recovery - // Server stores keygen_2 (P1), but client needs identifier for P0 + // Reconstruct public_key_package from user and server verifying_shares + const userIdentifier = participantToIdentifier(Participant.P0); + const serverIdentifier = participantToIdentifier(Participant.P1); + const verifyingKey = Array.from(wallet.public_key); + + let publicKeyPackageBytes: Uint8Array; + try { + publicKeyPackageBytes = reconstructPublicKeyPackageEd25519( + new Uint8Array(user_verifying_share), + new Uint8Array(userIdentifier), + new Uint8Array(storedShares.verifying_share), + new Uint8Array(serverIdentifier), + new Uint8Array(verifyingKey), + ); + } catch (error) { + return { + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to reconstruct public_key_package: ${error instanceof Error ? error.message : String(error)}`, + }; + } + return { success: true, data: { public_key: wallet.public_key.toString("hex"), - public_key_package: keyPackageData.public_key_package, + public_key_package: Array.from(publicKeyPackageBytes), identifier: participantToIdentifier(Participant.P0), }, }; diff --git a/backend/tss_api/src/routes/index.ts b/backend/tss_api/src/routes/index.ts index 183639b6a..4894fa75d 100644 --- a/backend/tss_api/src/routes/index.ts +++ b/backend/tss_api/src/routes/index.ts @@ -4,7 +4,6 @@ import { setKeygenRoutes } from "./keygen"; import { setKeygenEd25519Routes } from "./keygen_ed25519"; import { setTriplesRoutes } from "./triples"; import { setPresignRoutes } from "./presign"; -import { setPresignEd25519Routes } from "./presign_ed25519"; import { setSignRoutes } from "./sign"; import { setSignEd25519Routes } from "./sign_ed25519"; import { setWalletEd25519Routes } from "./wallet_ed25519"; @@ -19,7 +18,6 @@ export function makeTssRouter() { setKeygenEd25519Routes(router); setTriplesRoutes(router); setPresignRoutes(router); - setPresignEd25519Routes(router); setSignRoutes(router); setSignEd25519Routes(router); setWalletEd25519Routes(router); diff --git a/backend/tss_api/src/routes/presign_ed25519.ts b/backend/tss_api/src/routes/presign_ed25519.ts deleted file mode 100644 index 08efca0a0..000000000 --- a/backend/tss_api/src/routes/presign_ed25519.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Response, Router } from "express"; -import type { - PresignEd25519Body, - PresignEd25519Response, -} from "@oko-wallet/oko-types/tss"; -import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; -import { ErrorCodeMap } from "@oko-wallet/oko-api-error-codes"; -import { - ErrorResponseSchema, - UserAuthHeaderSchema, -} from "@oko-wallet/oko-api-openapi/common"; -import { registry } from "@oko-wallet/oko-api-openapi"; - -import { runPresignEd25519 } from "@oko-wallet-tss-api/api/presign_ed25519"; -import { - type UserAuthenticatedRequest, - userJwtMiddleware, - sendResponseWithNewToken, -} from "@oko-wallet-tss-api/middleware/keplr_auth"; -import { apiKeyMiddleware } from "@oko-wallet-tss-api/middleware/api_key_auth"; -import { tssActivateMiddleware } from "@oko-wallet-tss-api/middleware/tss_activate"; - -export function setPresignEd25519Routes(router: Router) { - registry.registerPath({ - method: "post", - path: "/tss/v1/presign_ed25519", - tags: ["TSS"], - summary: "Generate Ed25519 presign (nonces and commitments)", - description: - "Pre-generate nonces and commitments for Ed25519 threshold signing. " + - "This can be called before knowing the message to sign.", - security: [{ userAuth: [] }], - request: { - headers: UserAuthHeaderSchema, - body: { - required: false, - content: { - "application/json": { - schema: { - type: "object", - properties: {}, - }, - }, - }, - }, - }, - responses: { - 200: { - description: "Successfully generated presign", - content: { - "application/json": { - schema: { - type: "object", - properties: { - success: { type: "boolean" }, - data: { - type: "object", - properties: { - session_id: { type: "string" }, - commitments_0: { - type: "object", - properties: { - identifier: { - type: "array", - items: { type: "number" }, - }, - commitments: { - type: "array", - items: { type: "number" }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - 401: { - description: "Unauthorized", - content: { "application/json": { schema: ErrorResponseSchema } }, - }, - 500: { - description: "Internal server error", - content: { "application/json": { schema: ErrorResponseSchema } }, - }, - }, - }); - - router.post( - "/presign_ed25519", - [apiKeyMiddleware, userJwtMiddleware, tssActivateMiddleware], - async ( - req: UserAuthenticatedRequest, - res: Response>, - ) => { - const state = req.app.locals as any; - const user = res.locals.user; - const apiKey = res.locals.api_key; - - if (!user.wallet_id_ed25519) { - res.status(400).json({ - success: false, - code: "WALLET_NOT_FOUND", - msg: "Ed25519 wallet not found. Please create one first.", - }); - return; - } - - const result = await runPresignEd25519( - state.db, - state.encryption_secret, - { - email: user.email.toLowerCase(), - wallet_id: user.wallet_id_ed25519, - customer_id: apiKey.customer_id, - }, - ); - - if (result.success === false) { - res.status(ErrorCodeMap[result.code] ?? 500).json(result); - return; - } - - sendResponseWithNewToken(res, result.data); - }, - ); -} diff --git a/backend/tss_api/src/routes/sign_ed25519.ts b/backend/tss_api/src/routes/sign_ed25519.ts index 54374cf57..8986a47d2 100644 --- a/backend/tss_api/src/routes/sign_ed25519.ts +++ b/backend/tss_api/src/routes/sign_ed25519.ts @@ -6,8 +6,6 @@ import type { SignEd25519Round2Response, SignEd25519AggregateBody, SignEd25519AggregateResponse, - SignEd25519Body, - SignEd25519Response, } from "@oko-wallet/oko-types/tss"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import { ErrorCodeMap } from "@oko-wallet/oko-api-error-codes"; @@ -21,7 +19,6 @@ import { runSignEd25519Round1, runSignEd25519Round2, runSignEd25519Aggregate, - runSignEd25519, } from "@oko-wallet-tss-api/api/sign_ed25519"; import { type UserAuthenticatedRequest, @@ -315,43 +312,6 @@ export function setSignEd25519Routes(router: Router) { }, }); - router.post( - "/sign_ed25519", - [userJwtMiddleware, tssActivateMiddleware], - async ( - req: UserAuthenticatedRequest, - res: Response>, - ) => { - const state = req.app.locals as any; - const user = res.locals.user; - const body = req.body; - - if (!user.wallet_id_ed25519) { - res.status(400).json({ - success: false, - code: "WALLET_NOT_FOUND", - msg: "Ed25519 wallet not found. Please create one first.", - }); - return; - } - - const result = await runSignEd25519(state.db, state.encryption_secret, { - email: user.email.toLowerCase(), - wallet_id: user.wallet_id_ed25519, - session_id: body.session_id, - msg: body.msg, - commitments_1: body.commitments_1, - }); - - if (result.success === false) { - res.status(ErrorCodeMap[result.code] ?? 500).json(result); - return; - } - - sendResponseWithNewToken(res, result.data); - }, - ); - registry.registerPath({ method: "post", path: "/tss/v1/sign_ed25519/aggregate", @@ -368,6 +328,7 @@ export function setSignEd25519Routes(router: Router) { schema: { type: "object", properties: { + session_id: { type: "string", format: "uuid" }, msg: { type: "array", items: { type: "number" } }, all_commitments: { type: "array", @@ -392,8 +353,19 @@ export function setSignEd25519Routes(router: Router) { }, }, }, + user_verifying_share: { + type: "array", + items: { type: "number" }, + description: "P0's verifying_share (32 bytes)", + }, }, - required: ["msg", "all_commitments", "all_signature_shares"], + required: [ + "session_id", + "msg", + "all_commitments", + "all_signature_shares", + "user_verifying_share", + ], }, }, }, @@ -447,9 +419,11 @@ export function setSignEd25519Routes(router: Router) { { email: user.email.toLowerCase(), wallet_id: user.wallet_id, + session_id: body.session_id, msg: body.msg, all_commitments: body.all_commitments, all_signature_shares: body.all_signature_shares, + user_verifying_share: body.user_verifying_share, }, ); diff --git a/backend/tss_api/src/routes/wallet_ed25519.ts b/backend/tss_api/src/routes/wallet_ed25519.ts index a32ad738c..3ee93bd34 100644 --- a/backend/tss_api/src/routes/wallet_ed25519.ts +++ b/backend/tss_api/src/routes/wallet_ed25519.ts @@ -30,6 +30,24 @@ export function setWalletEd25519Routes(router: Router) { security: [{ oauthAuth: [] }], request: { headers: OAuthHeaderSchema, + body: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + user_verifying_share: { + type: "array", + items: { type: "number" }, + description: "P0's verifying_share (32 bytes)", + }, + }, + required: ["user_verifying_share"], + }, + }, + }, + }, }, responses: { 200: { @@ -76,7 +94,7 @@ export function setWalletEd25519Routes(router: Router) { oauthMiddleware, tssActivateMiddleware, async ( - req: OAuthAuthenticatedRequest>, + req: OAuthAuthenticatedRequest<{ user_verifying_share: number[] }>, res: Response< OkoApiResponse, OAuthLocals @@ -84,6 +102,7 @@ export function setWalletEd25519Routes(router: Router) { ) => { const state = req.app.locals; const oauthUser = res.locals.oauth_user; + const body = req.body; const user_identifier = oauthUser?.user_identifier; if (!user_identifier) { @@ -101,6 +120,7 @@ export function setWalletEd25519Routes(router: Router) { { user_identifier, auth_type: oauthUser.type, + user_verifying_share: body.user_verifying_share, }, ); diff --git a/common/oko_types/src/ct_dashboard/email_verification.ts b/common/oko_types/src/ct_dashboard/email_verification.ts index 2145516fa..84f657589 100644 --- a/common/oko_types/src/ct_dashboard/email_verification.ts +++ b/common/oko_types/src/ct_dashboard/email_verification.ts @@ -2,6 +2,7 @@ import type { SMTPConfig } from "../admin"; export enum EmailVerificationStatus { PENDING = "PENDING", + CODE_VERIFIED = "CODE_VERIFIED", VERIFIED = "VERIFIED", EXPIRED = "EXPIRED", } diff --git a/common/oko_types/src/tss/sign_ed25519.ts b/common/oko_types/src/tss/sign_ed25519.ts index 4a28f1445..221e04a52 100644 --- a/common/oko_types/src/tss/sign_ed25519.ts +++ b/common/oko_types/src/tss/sign_ed25519.ts @@ -38,9 +38,11 @@ export type SignEd25519Round2Body = { export interface SignEd25519AggregateRequest { email: string; wallet_id: string; + session_id: string; msg: number[]; all_commitments: CommitmentEntry[]; all_signature_shares: SignatureShareEntry[]; + user_verifying_share: number[]; // P0's verifying_share (32 bytes) } export interface SignEd25519AggregateResponse { @@ -48,43 +50,14 @@ export interface SignEd25519AggregateResponse { } export type SignEd25519AggregateBody = { + session_id: string; msg: number[]; all_commitments: CommitmentEntry[]; all_signature_shares: SignatureShareEntry[]; + user_verifying_share: number[]; }; export interface SignEd25519ServerState { nonces: number[]; identifier: number[]; } - -export interface PresignEd25519Request { - email: string; - wallet_id: string; - customer_id: string; -} - -export interface PresignEd25519Response { - session_id: string; - commitments_0: CommitmentEntry; -} - -export type PresignEd25519Body = Record; - -export interface SignEd25519Request { - email: string; - wallet_id: string; - session_id: string; - msg: number[]; - commitments_1: CommitmentEntry; -} - -export interface SignEd25519Response { - signature_share_0: SignatureShareEntry; -} - -export type SignEd25519Body = { - session_id: string; - msg: number[]; - commitments_1: CommitmentEntry; -}; diff --git a/common/oko_types/src/tss/tss_stage.ts b/common/oko_types/src/tss/tss_stage.ts index 65d129caa..7af8688ef 100644 --- a/common/oko_types/src/tss/tss_stage.ts +++ b/common/oko_types/src/tss/tss_stage.ts @@ -18,7 +18,6 @@ export enum TssStageType { PRESIGN = "PRESIGN", SIGN = "SIGN", SIGN_ED25519 = "SIGN_ED25519", - PRESIGN_ED25519 = "PRESIGN_ED25519", } interface TssStageBase { @@ -132,12 +131,6 @@ export type SignEd25519Stage = TssStageBase & { stage_data: SignEd25519StageData; }; -export type PresignEd25519Stage = TssStageBase & { - stage_type: TssStageType.PRESIGN_ED25519; - stage_status: PresignEd25519StageStatus; - stage_data: PresignEd25519StageData; -}; - export type TssStageStatus = | TriplesStageStatus | PresignStageStatus @@ -149,8 +142,7 @@ export type TssStage = | TriplesStage | PresignStage | SignStage - | SignEd25519Stage - | PresignEd25519Stage; + | SignEd25519Stage; export type CreateTssStageRequest = Pick< TssStage, diff --git a/common/oko_types/src/user_key_share/index.ts b/common/oko_types/src/user_key_share/index.ts index eae42744d..4e08e3e4f 100644 --- a/common/oko_types/src/user_key_share/index.ts +++ b/common/oko_types/src/user_key_share/index.ts @@ -1,4 +1,4 @@ -import type { Bytes32 } from "@oko-wallet/bytes"; +import { Bytes, type Bytes32, type Bytes64 } from "@oko-wallet/bytes"; export interface RunExpandSharesResult { t: number; @@ -24,3 +24,103 @@ export interface PointNumArr { x: number[]; y: number[]; } + +/** + * TEDDSA key share for KS Node storage. + * + * identifier: 32 bytes - SSS x-coordinate (node_name SHA256, byte[31] &= 0x0F) + * signing_share: 32 bytes - SSS y-coordinate (split signing share) + * + * Note: verifying_share is NOT stored. + * It can be recovered from signing_share via scalar_base_mult(). + * + * Total: 64 bytes (same size as Point256) + */ +export interface TeddsaKeyShare { + identifier: Bytes32; + signing_share: Bytes32; +} + +export interface TeddsaKeyShareByNode { + node: NodeNameAndEndpoint; + share: TeddsaKeyShare; +} + +/** + * Serialize TeddsaKeyShare to Bytes64. + * Format: identifier (32 bytes) || signing_share (32 bytes) + */ +export function teddsaKeyShareToBytes64(share: TeddsaKeyShare): Bytes64 { + const combined = new Uint8Array(64); + combined.set(share.identifier.toUint8Array(), 0); + combined.set(share.signing_share.toUint8Array(), 32); + + const result = Bytes.fromUint8Array(combined, 64); + if (!result.success) { + throw new Error(`Failed to create Bytes64: ${result.err}`); + } + return result.data; +} + +/** + * Deserialize Bytes64 to TeddsaKeyShare. + */ +export function bytes64ToTeddsaKeyShare(bytes: Bytes64): TeddsaKeyShare { + const arr = bytes.toUint8Array(); + + const identifierResult = Bytes.fromUint8Array(arr.slice(0, 32), 32); + if (!identifierResult.success) { + throw new Error(`Failed to extract identifier: ${identifierResult.err}`); + } + + const signingResult = Bytes.fromUint8Array(arr.slice(32, 64), 32); + if (!signingResult.success) { + throw new Error(`Failed to extract signing_share: ${signingResult.err}`); + } + + return { + identifier: identifierResult.data, + signing_share: signingResult.data, + }; +} + +/** + * Convert TeddsaKeyShare to 128-char hex string (for KS node API). + */ +export function teddsaKeyShareToHex(share: TeddsaKeyShare): string { + return teddsaKeyShareToBytes64(share).toHex(); +} + +/** + * Convert 128-char hex string to TeddsaKeyShare. + */ +export function hexToTeddsaKeyShare(hex: string): TeddsaKeyShare { + const bytesResult = Bytes.fromHexString(hex, 64); + if (!bytesResult.success) { + throw new Error( + `Invalid hex string for TeddsaKeyShare: ${bytesResult.err}`, + ); + } + return bytes64ToTeddsaKeyShare(bytesResult.data); +} + +/** + * Convert TeddsaKeyShare to Point256 (same structure, different semantics). + * Use when reusing existing Ed25519 functions. + */ +export function teddsaKeyShareToPoint256(share: TeddsaKeyShare): Point256 { + return { + x: share.identifier, + y: share.signing_share, + }; +} + +/** + * Convert Point256 to TeddsaKeyShare. + */ +export function point256ToTeddsaKeyShare(point: Point256): TeddsaKeyShare { + return { + identifier: point.x, + signing_share: point.y, + }; +} diff --git a/crypto/teddsa/api_lib/src/index.ts b/crypto/teddsa/api_lib/src/index.ts index f608e1ed8..78aa4f2c3 100644 --- a/crypto/teddsa/api_lib/src/index.ts +++ b/crypto/teddsa/api_lib/src/index.ts @@ -6,10 +6,6 @@ import type { SignEd25519Round2Response, SignEd25519AggregateBody, SignEd25519AggregateResponse, - PresignEd25519Body, - PresignEd25519Response, - SignEd25519Body, - SignEd25519Response, } from "@oko-wallet/oko-types/tss"; import type { SignInResponse } from "@oko-wallet/oko-types/user"; import type { @@ -178,33 +174,3 @@ export async function reqSignEd25519Aggregate( ); return resp; } - -export async function reqPresignEd25519( - endpoint: string, - payload: PresignEd25519Body, - apiKey: string, - authToken: string, -) { - const resp: OkoApiResponse = await makePostRequest( - endpoint, - "presign_ed25519", - payload, - authToken, - apiKey, - ); - return resp; -} - -export async function reqSignEd25519( - endpoint: string, - payload: SignEd25519Body, - authToken: string, -) { - const resp: OkoApiResponse = await makePostRequest( - endpoint, - "sign_ed25519", - payload, - authToken, - ); - return resp; -} diff --git a/crypto/teddsa/api_lib/tsconfig.json b/crypto/teddsa/api_lib/tsconfig.json index c8c92cbd6..1b280ecd1 100644 --- a/crypto/teddsa/api_lib/tsconfig.json +++ b/crypto/teddsa/api_lib/tsconfig.json @@ -1,8 +1,17 @@ { - "extends": "../../../tsconfig.json", "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" + "outDir": "dist", + "sourceMap": true, + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true }, - "include": ["src/**/*"] + "include": ["./*.js", "src/**/*.ts"] } diff --git a/crypto/teddsa/frost_ed25519_keplr/tests/integration_tests.rs b/crypto/teddsa/frost_ed25519_keplr/tests/integration_tests.rs index 77bb73716..8f53095a4 100644 --- a/crypto/teddsa/frost_ed25519_keplr/tests/integration_tests.rs +++ b/crypto/teddsa/frost_ed25519_keplr/tests/integration_tests.rs @@ -379,3 +379,153 @@ fn check_sign_with_incorrect_commitments() { rng, ); } + +/// Test to verify the relationship between identifier and verifying_share. +/// +/// In FROST Ed25519: +/// - identifier: SSS polynomial x-coordinate (scalar, 32 bytes) +/// - signing_share: f(identifier) where f is secret polynomial (scalar, 32 bytes) +/// - verifying_share: signing_share * G (EdwardsPoint, compressed 32 bytes) +/// +/// The compressed EdwardsY format stores: y-coordinate (255 bits) + x sign bit (1 bit) +/// This test extracts the actual x,y coordinates from verifying_share and verifies +/// that identifier is NOT the x-coordinate of the point (it's the polynomial x-value). +#[test] +fn check_identifier_is_not_verifying_share_x_coordinate() { + use curve25519_dalek::edwards::CompressedEdwardsY; + + let mut rng = rand::rngs::OsRng; + + // Generate keys with dealer + let max_signers = 3; + let min_signers = 2; + let (shares, pubkeys) = keys::generate_with_dealer( + max_signers, + min_signers, + keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + // For each participant, check the relationship + for (identifier, secret_share) in &shares { + // Get the key package + let key_package: keys::KeyPackage = secret_share.clone().try_into().unwrap(); + + // Get verifying_share bytes (compressed EdwardsY format) + let verifying_share_bytes = key_package.verifying_share().serialize().unwrap(); + assert_eq!( + verifying_share_bytes.len(), + 32, + "verifying_share should be 32 bytes" + ); + + // Decompress to get actual point coordinates + let compressed = CompressedEdwardsY::from_slice(&verifying_share_bytes).unwrap(); + let point = compressed.decompress().expect("valid point"); + + // Get the actual x and y coordinates (as field elements, in bytes) + // EdwardsPoint internally stores (X, Y, Z, T) in extended coordinates + // To get affine x, y: x = X/Z, y = Y/Z + let point_bytes = point.compress().to_bytes(); + + // Get identifier bytes + let identifier_bytes = identifier.serialize(); + assert_eq!(identifier_bytes.len(), 32, "identifier should be 32 bytes"); + + // The compressed format is y-coordinate with x's sign bit in the MSB + // So the first 255 bits are y, and bit 255 is x's sign + // identifier is a scalar (the x-value in SSS polynomial), NOT the point's x-coordinate + + // Verify that identifier != compressed point bytes (they should be different) + assert_ne!( + identifier_bytes, verifying_share_bytes, + "identifier should NOT equal verifying_share compressed bytes" + ); + + // Also verify that signing_share * G = verifying_share + let signing_share_bytes = key_package.signing_share().serialize(); + assert_eq!( + signing_share_bytes.len(), + 32, + "signing_share should be 32 bytes" + ); + + // The verifying_share from pubkeys should match + let pubkey_verifying_share = pubkeys.verifying_shares().get(identifier).unwrap(); + assert_eq!( + key_package.verifying_share().serialize().unwrap(), + pubkey_verifying_share.serialize().unwrap(), + "verifying_share should match in key_package and public_key_package" + ); + + println!("Participant {:?}:", identifier); + println!( + " identifier bytes: {}", + hex::encode(&identifier_bytes) + ); + println!( + " signing_share bytes: {}", + hex::encode(&signing_share_bytes) + ); + println!( + " verifying_share bytes: {}", + hex::encode(&verifying_share_bytes) + ); + } +} + +/// Test to extract x-coordinate from verifying_share (compressed EdwardsY point). +/// +/// Ed25519 compressed format: y-coordinate (255 bits) + x sign bit (1 bit) = 32 bytes +/// To get x-coordinate, we need to decompress the point. +#[test] +fn check_extract_x_coordinate_from_verifying_share() { + use curve25519_dalek::edwards::CompressedEdwardsY; + + let mut rng = rand::rngs::OsRng; + + let max_signers = 3; + let min_signers = 2; + let (shares, _pubkeys) = keys::generate_with_dealer( + max_signers, + min_signers, + keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + for (identifier, secret_share) in &shares { + let key_package: keys::KeyPackage = secret_share.clone().try_into().unwrap(); + + // Get verifying_share (compressed point) + let verifying_share_bytes = key_package.verifying_share().serialize().unwrap(); + + // Decompress + let compressed = CompressedEdwardsY::from_slice(&verifying_share_bytes).unwrap(); + let point = compressed.decompress().expect("valid point"); + + // Extract y-coordinate from compressed bytes (first 255 bits) + let mut y_bytes = verifying_share_bytes.clone(); + y_bytes[31] &= 0x7F; // Clear the sign bit to get pure y + + // The x sign bit + let x_sign = (verifying_share_bytes[31] >> 7) & 1; + + println!("Participant {:?}:", identifier); + println!( + " compressed bytes: {}", + hex::encode(&verifying_share_bytes) + ); + println!(" y-coordinate: {}", hex::encode(&y_bytes)); + println!(" x sign bit: {}", x_sign); + + // Verify the point is valid by recompressing + let recompressed = point.compress(); + assert_eq!( + recompressed.to_bytes(), + verifying_share_bytes.as_slice(), + "recompressed point should match original" + ); + } +} diff --git a/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/mod.rs b/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/mod.rs index a14896e17..c1d6cbee6 100644 --- a/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/mod.rs +++ b/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/mod.rs @@ -1,7 +1,9 @@ mod identifier; mod key_package; mod public_key_package; +mod scalar_mult; pub use identifier::*; pub use key_package::*; pub use public_key_package::*; +pub use scalar_mult::*; diff --git a/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/scalar_mult.rs b/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/scalar_mult.rs new file mode 100644 index 000000000..d23c78d14 --- /dev/null +++ b/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/src/keys/scalar_mult.rs @@ -0,0 +1,78 @@ +use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; +use curve25519_dalek::scalar::Scalar; +use gloo_utils::format::JsValueSerdeExt; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[derive(Serialize, Deserialize)] +pub struct ScalarBaseMultInput { + pub scalar: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct ScalarBaseMultOutput { + pub point: Vec, +} + +/// Compute verifying_share (point) from signing_share (scalar). +/// verifying_share = signing_share * G (Ed25519 base point) +#[wasm_bindgen] +pub fn scalar_base_mult(input: JsValue) -> Result { + let input: ScalarBaseMultInput = input + .into_serde() + .map_err(|e| JsValue::from_str(&format!("Failed to parse input: {}", e)))?; + + let result = + scalar_base_mult_inner(&input).map_err(|e| JsValue::from_str(&format!("{}", e)))?; + + JsValue::from_serde(&result) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize output: {}", e))) +} + +fn scalar_base_mult_inner(input: &ScalarBaseMultInput) -> Result { + if input.scalar.len() != 32 { + return Err(format!( + "Invalid scalar length: expected 32, got {}", + input.scalar.len() + )); + } + + let scalar_bytes: [u8; 32] = input + .scalar + .clone() + .try_into() + .map_err(|_| "Failed to convert scalar bytes")?; + + let scalar = Scalar::from_bytes_mod_order(scalar_bytes); + let point = &scalar * ED25519_BASEPOINT_TABLE; + let point_bytes = point.compress().to_bytes(); + + Ok(ScalarBaseMultOutput { + point: point_bytes.to_vec(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_scalar_base_mult_valid() { + let mut scalar = vec![0u8; 32]; + scalar[0] = 1; + + let input = ScalarBaseMultInput { scalar }; + let result = scalar_base_mult_inner(&input).unwrap(); + + assert_eq!(result.point.len(), 32); + } + + #[test] + fn test_scalar_base_mult_invalid_length() { + let input = ScalarBaseMultInput { + scalar: vec![1u8; 16], + }; + let result = scalar_base_mult_inner(&input); + assert!(result.is_err()); + } +} diff --git a/crypto/teddsa/teddsa_addon/addon/index.d.ts b/crypto/teddsa/teddsa_addon/addon/index.d.ts index 9929abd1b..5f78ce633 100644 --- a/crypto/teddsa/teddsa_addon/addon/index.d.ts +++ b/crypto/teddsa/teddsa_addon/addon/index.d.ts @@ -19,6 +19,17 @@ export interface NapiCentralizedKeygenOutput { export declare function napiKeygenCentralizedEd25519(): NapiCentralizedKeygenOutput /** Import an existing Ed25519 secret key and split it into threshold shares. */ export declare function napiKeygenImportEd25519(secretKey: Array): NapiCentralizedKeygenOutput +/** Extract signing_share and verifying_share from a serialized key_package. */ +export interface NapiKeyPackageShares { + signing_share: Array + verifying_share: Array +} +/** Extract signing_share and verifying_share from a serialized Ed25519 key_package. */ +export declare function napiExtractKeyPackageSharesEd25519(keyPackageBytes: Array): NapiKeyPackageShares +/** Reconstruct a key_package from signing_share, verifying_share, identifier, and verifying_key. */ +export declare function napiReconstructKeyPackageEd25519(signingShare: Array, verifyingShare: Array, identifier: Array, verifyingKey: Array, minSigners: number): Array +/** Reconstruct a public_key_package from verifying_shares, identifiers, and verifying_key. */ +export declare function napiReconstructPublicKeyPackageEd25519(clientVerifyingShare: Array, clientIdentifier: Array, serverVerifyingShare: Array, serverIdentifier: Array, verifyingKey: Array): Array /** Output from a signing round 1 (commitment) */ export interface NapiSigningCommitmentOutput { nonces: Array diff --git a/crypto/teddsa/teddsa_addon/addon/index.js b/crypto/teddsa/teddsa_addon/addon/index.js index 115a4f1d0..779f42f18 100644 --- a/crypto/teddsa/teddsa_addon/addon/index.js +++ b/crypto/teddsa/teddsa_addon/addon/index.js @@ -310,10 +310,13 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { napiKeygenCentralizedEd25519, napiKeygenImportEd25519, napiSignRound1Ed25519, napiSignRound2Ed25519, napiAggregateEd25519, napiVerifyEd25519 } = nativeBinding +const { napiKeygenCentralizedEd25519, napiKeygenImportEd25519, napiExtractKeyPackageSharesEd25519, napiReconstructKeyPackageEd25519, napiReconstructPublicKeyPackageEd25519, napiSignRound1Ed25519, napiSignRound2Ed25519, napiAggregateEd25519, napiVerifyEd25519 } = nativeBinding module.exports.napiKeygenCentralizedEd25519 = napiKeygenCentralizedEd25519 module.exports.napiKeygenImportEd25519 = napiKeygenImportEd25519 +module.exports.napiExtractKeyPackageSharesEd25519 = napiExtractKeyPackageSharesEd25519 +module.exports.napiReconstructKeyPackageEd25519 = napiReconstructKeyPackageEd25519 +module.exports.napiReconstructPublicKeyPackageEd25519 = napiReconstructPublicKeyPackageEd25519 module.exports.napiSignRound1Ed25519 = napiSignRound1Ed25519 module.exports.napiSignRound2Ed25519 = napiSignRound2Ed25519 module.exports.napiAggregateEd25519 = napiAggregateEd25519 diff --git a/crypto/teddsa/teddsa_addon/addon/src/keygen.rs b/crypto/teddsa/teddsa_addon/addon/src/keygen.rs index b10ac33a9..3a9b36c5b 100644 --- a/crypto/teddsa/teddsa_addon/addon/src/keygen.rs +++ b/crypto/teddsa/teddsa_addon/addon/src/keygen.rs @@ -64,7 +64,9 @@ fn keygen_centralized_inner() -> std::result::Result std::result::Result { +fn keygen_import_inner( + secret: [u8; 32], +) -> std::result::Result { let mut rng = OsRng; let max_signers = 2; let min_signers = 2; @@ -136,3 +138,168 @@ pub fn napi_keygen_import_ed25519(secret_key: Vec) -> Result, + #[napi(js_name = "verifying_share")] + pub verifying_share: Vec, +} + +/// Extract signing_share and verifying_share from a serialized Ed25519 key_package. +#[napi] +pub fn napi_extract_key_package_shares_ed25519( + key_package_bytes: Vec, +) -> Result { + let key_package = KeyPackage::deserialize(&key_package_bytes).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize key_package: {:?}", e), + ) + })?; + + let signing_share_bytes = key_package.signing_share().serialize(); + let verifying_share_bytes = key_package + .verifying_share() + .serialize() + .map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to serialize verifying_share: {:?}", e), + ) + })? + .to_vec(); + + Ok(NapiKeyPackageShares { + signing_share: signing_share_bytes.to_vec(), + verifying_share: verifying_share_bytes, + }) +} + +/// Reconstruct a key_package from signing_share, verifying_share, identifier, and verifying_key. +#[napi] +pub fn napi_reconstruct_key_package_ed25519( + signing_share: Vec, + verifying_share: Vec, + identifier: Vec, + verifying_key: Vec, + min_signers: u16, +) -> Result> { + use frost::keys::{SigningShare, VerifyingShare}; + use frost::Identifier; + use frost::VerifyingKey; + + let identifier = Identifier::deserialize(&identifier).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize identifier: {:?}", e), + ) + })?; + + let signing_share = SigningShare::deserialize(&signing_share).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize signing_share: {:?}", e), + ) + })?; + + let verifying_share = VerifyingShare::deserialize(&verifying_share).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize verifying_share: {:?}", e), + ) + })?; + + let verifying_key = VerifyingKey::deserialize(&verifying_key).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize verifying_key: {:?}", e), + ) + })?; + + let key_package = KeyPackage::new( + identifier, + signing_share, + verifying_share, + verifying_key, + min_signers, + ); + + let key_package_bytes = key_package.serialize().map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to serialize key_package: {:?}", e), + ) + })?; + + Ok(key_package_bytes) +} + +/// Reconstruct a public_key_package from verifying_shares, identifiers, and verifying_key. +#[napi] +pub fn napi_reconstruct_public_key_package_ed25519( + client_verifying_share: Vec, + client_identifier: Vec, + server_verifying_share: Vec, + server_identifier: Vec, + verifying_key: Vec, +) -> Result> { + use frost::keys::{PublicKeyPackage, VerifyingShare}; + use frost::Identifier; + use frost::VerifyingKey; + use std::collections::BTreeMap; + + let client_identifier = Identifier::deserialize(&client_identifier).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize client_identifier: {:?}", e), + ) + })?; + + let server_identifier = Identifier::deserialize(&server_identifier).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize server_identifier: {:?}", e), + ) + })?; + + let client_verifying_share = + VerifyingShare::deserialize(&client_verifying_share).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize client_verifying_share: {:?}", e), + ) + })?; + + let server_verifying_share = + VerifyingShare::deserialize(&server_verifying_share).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize server_verifying_share: {:?}", e), + ) + })?; + + let verifying_key = VerifyingKey::deserialize(&verifying_key).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to deserialize verifying_key: {:?}", e), + ) + })?; + + let mut verifying_shares: BTreeMap = BTreeMap::new(); + verifying_shares.insert(client_identifier, client_verifying_share); + verifying_shares.insert(server_identifier, server_verifying_share); + + let public_key_package = PublicKeyPackage::new(verifying_shares, verifying_key); + + let public_key_package_bytes = public_key_package.serialize().map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to serialize public_key_package: {:?}", e), + ) + })?; + + Ok(public_key_package_bytes) +} diff --git a/crypto/teddsa/teddsa_addon/addon/teddsa-addon.darwin-arm64.node b/crypto/teddsa/teddsa_addon/addon/teddsa-addon.darwin-arm64.node index f0b9be053..e1abb4b8d 100755 Binary files a/crypto/teddsa/teddsa_addon/addon/teddsa-addon.darwin-arm64.node and b/crypto/teddsa/teddsa_addon/addon/teddsa-addon.darwin-arm64.node differ diff --git a/crypto/teddsa/teddsa_addon/src/server/index.ts b/crypto/teddsa/teddsa_addon/src/server/index.ts index 9c7c73d1a..ad93c8ef4 100644 --- a/crypto/teddsa/teddsa_addon/src/server/index.ts +++ b/crypto/teddsa/teddsa_addon/src/server/index.ts @@ -11,6 +11,9 @@ import { napiSignRound2Ed25519, napiAggregateEd25519, napiVerifyEd25519, + napiExtractKeyPackageSharesEd25519, + napiReconstructKeyPackageEd25519, + napiReconstructPublicKeyPackageEd25519, } from "../../addon/index.js"; // NOTE: NAPI-specific types (serialized bytes format) @@ -41,6 +44,11 @@ export interface NapiSignatureOutput { signature: number[]; } +export interface NapiKeyPackageShares { + signing_share: number[]; + verifying_share: number[]; +} + export function runKeygenCentralizedEd25519(): NapiCentralizedKeygenOutput { return napiKeygenCentralizedEd25519(); } @@ -96,3 +104,45 @@ export function runVerifyEd25519( Array.from(publicKeyPackage), ); } + +export function extractKeyPackageSharesEd25519( + keyPackage: Uint8Array, +): NapiKeyPackageShares { + return napiExtractKeyPackageSharesEd25519(Array.from(keyPackage)); +} + +export function reconstructKeyPackageEd25519( + signingShare: Uint8Array, + verifyingShare: Uint8Array, + identifier: Uint8Array, + verifyingKey: Uint8Array, + minSigners: number, +): Uint8Array { + return new Uint8Array( + napiReconstructKeyPackageEd25519( + Array.from(signingShare), + Array.from(verifyingShare), + Array.from(identifier), + Array.from(verifyingKey), + minSigners, + ), + ); +} + +export function reconstructPublicKeyPackageEd25519( + clientVerifyingShare: Uint8Array, + clientIdentifier: Uint8Array, + serverVerifyingShare: Uint8Array, + serverIdentifier: Uint8Array, + verifyingKey: Uint8Array, +): Uint8Array { + return new Uint8Array( + napiReconstructPublicKeyPackageEd25519( + Array.from(clientVerifyingShare), + Array.from(clientIdentifier), + Array.from(serverVerifyingShare), + Array.from(serverIdentifier), + Array.from(verifyingKey), + ), + ); +} diff --git a/crypto/teddsa/teddsa_addon/src/tests/keygen.test.ts b/crypto/teddsa/teddsa_addon/src/tests/keygen.test.ts index 95460b2d8..8e07204aa 100644 --- a/crypto/teddsa/teddsa_addon/src/tests/keygen.test.ts +++ b/crypto/teddsa/teddsa_addon/src/tests/keygen.test.ts @@ -1,6 +1,12 @@ import { Participant } from "@oko-wallet/teddsa-interface"; -import { runKeygenCentralizedEd25519, runKeygenImportEd25519 } from "../server"; +import { + runKeygenCentralizedEd25519, + runKeygenImportEd25519, + extractKeyPackageSharesEd25519, + reconstructPublicKeyPackageEd25519, + reconstructKeyPackageEd25519, +} from "../server"; export async function keygenCentralizedTest() { console.log("Testing centralized keygen...\n"); @@ -353,6 +359,562 @@ export async function keygenCentralizedConsistencyTest() { console.log("Consistency test passed"); } +export async function extractKeyPackageSharesTest() { + console.log("\nTesting extractKeyPackageSharesEd25519...\n"); + + // Generate a key_package first + const keygenOutput = runKeygenCentralizedEd25519(); + const clientKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P0].key_package, + ); + const serverKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P1].key_package, + ); + + // Extract shares from client key_package + const clientShares = extractKeyPackageSharesEd25519(clientKeyPackage); + + // Validate output structure + if (!clientShares.signing_share || clientShares.signing_share.length !== 32) { + throw new Error( + `Expected 32-byte signing_share, got ${clientShares.signing_share?.length ?? 0} bytes`, + ); + } + + if ( + !clientShares.verifying_share || + clientShares.verifying_share.length !== 32 + ) { + throw new Error( + `Expected 32-byte verifying_share, got ${clientShares.verifying_share?.length ?? 0} bytes`, + ); + } + + console.log(" ✓ Client shares extracted successfully"); + console.log(` ✓ Signing share: ${clientShares.signing_share.length} bytes`); + console.log( + ` ✓ Verifying share: ${clientShares.verifying_share.length} bytes`, + ); + + // Extract shares from server key_package + const serverShares = extractKeyPackageSharesEd25519(serverKeyPackage); + + // Validate server shares + if (!serverShares.signing_share || serverShares.signing_share.length !== 32) { + throw new Error( + `Expected 32-byte signing_share, got ${serverShares.signing_share?.length ?? 0} bytes`, + ); + } + + if ( + !serverShares.verifying_share || + serverShares.verifying_share.length !== 32 + ) { + throw new Error( + `Expected 32-byte verifying_share, got ${serverShares.verifying_share?.length ?? 0} bytes`, + ); + } + + console.log(" ✓ Server shares extracted successfully"); + + // Verify that client and server shares are different + const clientSigningShareHex = Buffer.from( + clientShares.signing_share, + ).toString("hex"); + const serverSigningShareHex = Buffer.from( + serverShares.signing_share, + ).toString("hex"); + if (clientSigningShareHex === serverSigningShareHex) { + throw new Error("Client and server signing shares should be different"); + } + + const clientVerifyingShareHex = Buffer.from( + clientShares.verifying_share, + ).toString("hex"); + const serverVerifyingShareHex = Buffer.from( + serverShares.verifying_share, + ).toString("hex"); + if (clientVerifyingShareHex === serverVerifyingShareHex) { + throw new Error("Client and server verifying shares should be different"); + } + + console.log(" ✓ Client and server shares are different"); + + // Test with invalid key_package + try { + const invalidKeyPackage = new Uint8Array(32).fill(0xff); + extractKeyPackageSharesEd25519(invalidKeyPackage); + throw new Error("Should have thrown an error for invalid key_package"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") + ) { + throw new Error( + `Unexpected error for invalid key_package: ${error.message}`, + ); + } + console.log(" ✓ Invalid key_package correctly rejected"); + } + + // Test consistency: extract from same key_package multiple times + const shares1 = extractKeyPackageSharesEd25519(clientKeyPackage); + const shares2 = extractKeyPackageSharesEd25519(clientKeyPackage); + + const signingShare1Hex = Buffer.from(shares1.signing_share).toString("hex"); + const signingShare2Hex = Buffer.from(shares2.signing_share).toString("hex"); + if (signingShare1Hex !== signingShare2Hex) { + throw new Error("Extraction should be deterministic"); + } + + const verifyingShare1Hex = Buffer.from(shares1.verifying_share).toString( + "hex", + ); + const verifyingShare2Hex = Buffer.from(shares2.verifying_share).toString( + "hex", + ); + if (verifyingShare1Hex !== verifyingShare2Hex) { + throw new Error("Extraction should be deterministic"); + } + + console.log(" ✓ Extraction is deterministic"); + + console.log("\nExtract key package shares test passed"); +} + +export async function reconstructPublicKeyPackageTest() { + console.log("\nTesting reconstructPublicKeyPackageEd25519...\n"); + + // Generate a keygen output to get the original public_key_package + const keygenOutput = runKeygenCentralizedEd25519(); + const originalPublicKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P0].public_key_package, + ); + + // Extract verifying_shares and identifiers from key_packages + const clientKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P0].key_package, + ); + const serverKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P1].key_package, + ); + + const clientShares = extractKeyPackageSharesEd25519(clientKeyPackage); + const serverShares = extractKeyPackageSharesEd25519(serverKeyPackage); + + const clientIdentifier = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P0].identifier, + ); + const serverIdentifier = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P1].identifier, + ); + const verifyingKey = new Uint8Array(keygenOutput.public_key); + + // Reconstruct public_key_package + const reconstructedPublicKeyPackage = reconstructPublicKeyPackageEd25519( + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + verifyingKey, + ); + + // Validate output structure + if ( + !reconstructedPublicKeyPackage || + reconstructedPublicKeyPackage.length === 0 + ) { + throw new Error("Reconstructed public_key_package is empty"); + } + + console.log( + ` ✓ Public key package reconstructed: ${reconstructedPublicKeyPackage.length} bytes`, + ); + + // Compare with original public_key_package + const originalHex = Buffer.from(originalPublicKeyPackage).toString("hex"); + const reconstructedHex = Buffer.from(reconstructedPublicKeyPackage).toString( + "hex", + ); + + if (originalHex !== reconstructedHex) { + throw new Error( + "Reconstructed public_key_package should match original public_key_package", + ); + } + + console.log(" ✓ Reconstructed public_key_package matches original"); + + // Test consistency: reconstruct multiple times should produce same result + const reconstructed1 = reconstructPublicKeyPackageEd25519( + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + verifyingKey, + ); + const reconstructed2 = reconstructPublicKeyPackageEd25519( + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + verifyingKey, + ); + + const reconstructed1Hex = Buffer.from(reconstructed1).toString("hex"); + const reconstructed2Hex = Buffer.from(reconstructed2).toString("hex"); + + if (reconstructed1Hex !== reconstructed2Hex) { + throw new Error("Reconstruction should be deterministic"); + } + + console.log(" ✓ Reconstruction is deterministic"); + + // Test with invalid verifying_share (wrong length) + try { + const invalidVerifyingShare = new Uint8Array(31).fill(0xff); + reconstructPublicKeyPackageEd25519( + invalidVerifyingShare, + clientIdentifier, + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + verifyingKey, + ); + throw new Error("Should have thrown an error for invalid verifying_share"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid verifying_share: ${error.message}`, + ); + } + console.log(" ✓ Invalid verifying_share correctly rejected"); + } + + // Test with invalid identifier (wrong length) + try { + const invalidIdentifier = new Uint8Array(31).fill(0xff); + reconstructPublicKeyPackageEd25519( + new Uint8Array(clientShares.verifying_share), + invalidIdentifier, + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + verifyingKey, + ); + throw new Error("Should have thrown an error for invalid identifier"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid identifier: ${error.message}`, + ); + } + console.log(" ✓ Invalid identifier correctly rejected"); + } + + // Test with invalid verifying_key (wrong length) + try { + const invalidVerifyingKey = new Uint8Array(31).fill(0xff); + reconstructPublicKeyPackageEd25519( + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + invalidVerifyingKey, + ); + throw new Error("Should have thrown an error for invalid verifying_key"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid verifying_key: ${error.message}`, + ); + } + console.log(" ✓ Invalid verifying_key correctly rejected"); + } + + // Test with swapped identifiers (should produce same result since BTreeMap orders by identifier) + const swappedPublicKeyPackage = reconstructPublicKeyPackageEd25519( + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + verifyingKey, + ); + + const swappedHex = Buffer.from(swappedPublicKeyPackage).toString("hex"); + if (swappedHex !== originalHex) { + throw new Error( + "Swapped identifiers should produce same public_key_package (BTreeMap orders by identifier)", + ); + } + + console.log( + " ✓ Swapped identifiers produce same package (BTreeMap ordering)", + ); + + console.log("\nReconstruct public key package test passed"); +} + +export async function reconstructKeyPackageTest() { + console.log("\nTesting reconstructKeyPackageEd25519...\n"); + + // Generate a keygen output to get the original key_package + const keygenOutput = runKeygenCentralizedEd25519(); + const originalClientKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P0].key_package, + ); + const originalServerKeyPackage = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P1].key_package, + ); + + // Extract shares from key_packages + const clientShares = extractKeyPackageSharesEd25519(originalClientKeyPackage); + const serverShares = extractKeyPackageSharesEd25519(originalServerKeyPackage); + + const clientIdentifier = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P0].identifier, + ); + const serverIdentifier = new Uint8Array( + keygenOutput.keygen_outputs[Participant.P1].identifier, + ); + const verifyingKey = new Uint8Array(keygenOutput.public_key); + const minSigners = 2; + + // Reconstruct client key_package + const reconstructedClientKeyPackage = reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + verifyingKey, + minSigners, + ); + + // Validate output structure + if ( + !reconstructedClientKeyPackage || + reconstructedClientKeyPackage.length === 0 + ) { + throw new Error("Reconstructed client key_package is empty"); + } + + console.log( + ` ✓ Client key package reconstructed: ${reconstructedClientKeyPackage.length} bytes`, + ); + + // Compare with original client key_package + const originalClientHex = Buffer.from(originalClientKeyPackage).toString( + "hex", + ); + const reconstructedClientHex = Buffer.from( + reconstructedClientKeyPackage, + ).toString("hex"); + + if (originalClientHex !== reconstructedClientHex) { + throw new Error( + "Reconstructed client key_package should match original client key_package", + ); + } + + console.log(" ✓ Reconstructed client key_package matches original"); + + // Reconstruct server key_package + const reconstructedServerKeyPackage = reconstructKeyPackageEd25519( + new Uint8Array(serverShares.signing_share), + new Uint8Array(serverShares.verifying_share), + serverIdentifier, + verifyingKey, + minSigners, + ); + + // Validate server key_package + if ( + !reconstructedServerKeyPackage || + reconstructedServerKeyPackage.length === 0 + ) { + throw new Error("Reconstructed server key_package is empty"); + } + + const originalServerHex = Buffer.from(originalServerKeyPackage).toString( + "hex", + ); + const reconstructedServerHex = Buffer.from( + reconstructedServerKeyPackage, + ).toString("hex"); + + if (originalServerHex !== reconstructedServerHex) { + throw new Error( + "Reconstructed server key_package should match original server key_package", + ); + } + + console.log(" ✓ Reconstructed server key_package matches original"); + + // Verify client and server key_packages are different + if (reconstructedClientHex === reconstructedServerHex) { + throw new Error( + "Client and server key_packages should be different (different identifiers and shares)", + ); + } + + console.log(" ✓ Client and server key_packages are different"); + + // Test consistency: reconstruct multiple times should produce same result + const reconstructed1 = reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + verifyingKey, + minSigners, + ); + const reconstructed2 = reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + verifyingKey, + minSigners, + ); + + const reconstructed1Hex = Buffer.from(reconstructed1).toString("hex"); + const reconstructed2Hex = Buffer.from(reconstructed2).toString("hex"); + + if (reconstructed1Hex !== reconstructed2Hex) { + throw new Error("Reconstruction should be deterministic"); + } + + console.log(" ✓ Reconstruction is deterministic"); + + // Test with invalid signing_share (wrong length) + try { + const invalidSigningShare = new Uint8Array(31).fill(0xff); + reconstructKeyPackageEd25519( + invalidSigningShare, + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + verifyingKey, + minSigners, + ); + throw new Error("Should have thrown an error for invalid signing_share"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid signing_share: ${error.message}`, + ); + } + console.log(" ✓ Invalid signing_share correctly rejected"); + } + + // Test with invalid verifying_share (wrong length) + try { + const invalidVerifyingShare = new Uint8Array(31).fill(0xff); + reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + invalidVerifyingShare, + clientIdentifier, + verifyingKey, + minSigners, + ); + throw new Error("Should have thrown an error for invalid verifying_share"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid verifying_share: ${error.message}`, + ); + } + console.log(" ✓ Invalid verifying_share correctly rejected"); + } + + // Test with invalid identifier (wrong length) + try { + const invalidIdentifier = new Uint8Array(31).fill(0xff); + reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + new Uint8Array(clientShares.verifying_share), + invalidIdentifier, + verifyingKey, + minSigners, + ); + throw new Error("Should have thrown an error for invalid identifier"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid identifier: ${error.message}`, + ); + } + console.log(" ✓ Invalid identifier correctly rejected"); + } + + // Test with invalid verifying_key (wrong length) + try { + const invalidVerifyingKey = new Uint8Array(31).fill(0xff); + reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + new Uint8Array(clientShares.verifying_share), + clientIdentifier, + invalidVerifyingKey, + minSigners, + ); + throw new Error("Should have thrown an error for invalid verifying_key"); + } catch (error: any) { + if ( + !error.message?.includes("deserialize") && + !error.message?.includes("Failed to deserialize") && + !error.message?.includes("Invalid") + ) { + throw new Error( + `Unexpected error for invalid verifying_key: ${error.message}`, + ); + } + console.log(" ✓ Invalid verifying_key correctly rejected"); + } + + const mismatchedKeyPackage = reconstructKeyPackageEd25519( + new Uint8Array(clientShares.signing_share), + new Uint8Array(clientShares.verifying_share), + serverIdentifier, // Using server identifier with client shares + verifyingKey, + minSigners, + ); + + if (mismatchedKeyPackage.length === 0) { + throw new Error( + "Mismatched components should still produce valid key_package", + ); + } + + const mismatchedHex = Buffer.from(mismatchedKeyPackage).toString("hex"); + if (mismatchedHex === originalClientHex) { + throw new Error( + "Mismatched identifier should produce different key_package", + ); + } + + console.log(" ✓ Mismatched components produce different key_package"); + + console.log("\nReconstruct key package test passed"); +} + // Run the tests async function main() { console.log("Starting Ed25519 keygen tests...\n"); @@ -366,6 +928,9 @@ async function main() { await keygenImportErrorTest(); await keygenImportEdgeCasesTest(); await keygenCentralizedConsistencyTest(); + await extractKeyPackageSharesTest(); + await reconstructKeyPackageTest(); + await reconstructPublicKeyPackageTest(); console.log("\n" + "=".repeat(50)); console.log("All keygen tests passed!"); diff --git a/crypto/teddsa/teddsa_hooks/package.json b/crypto/teddsa/teddsa_hooks/package.json index 744efbca8..9f2b0c40e 100644 --- a/crypto/teddsa/teddsa_hooks/package.json +++ b/crypto/teddsa/teddsa_hooks/package.json @@ -6,6 +6,7 @@ "@oko-wallet/bytes": "^0.0.3-alpha.65", "@oko-wallet/frost-ed25519-keplr-wasm": "workspace:*", "@oko-wallet/stdlib-js": "^0.0.2-rc.42", + "@oko-wallet/teddsa-api-lib": "workspace:*", "@oko-wallet/teddsa-interface": "workspace:*", "@oko-wallet/teddsa-wasm-mock": "workspace:*" }, diff --git a/crypto/teddsa/teddsa_hooks/src/sign.ts b/crypto/teddsa/teddsa_hooks/src/sign.ts index 786a69106..c67f26400 100644 --- a/crypto/teddsa/teddsa_hooks/src/sign.ts +++ b/crypto/teddsa/teddsa_hooks/src/sign.ts @@ -9,6 +9,11 @@ import type { KeyPackageRaw, PublicKeyPackageRaw, } from "@oko-wallet/teddsa-interface"; +import { + reqSignEd25519Round1, + reqSignEd25519Round2, + reqSignEd25519Aggregate, +} from "@oko-wallet/teddsa-api-lib"; import type { TeddsaKeygenOutputBytes } from "./types"; @@ -104,6 +109,134 @@ export function teddsaVerify( } } +export async function runTeddsaSign( + endpoint: string, + message: Uint8Array, + keyPackage: KeyPackageRaw, + authToken: string, + getIsAborted: () => boolean, +): Promise> { + if (getIsAborted()) { + return { success: false, err: { type: "aborted" } }; + } + + // Round 1: Generate client commitments locally + const round1Result = teddsaSignRound1(keyPackage); + if (!round1Result.success) { + return { success: false, err: { type: "error", msg: round1Result.err } }; + } + + const clientCommitment: CommitmentEntry = { + identifier: round1Result.data.identifier, + commitments: round1Result.data.commitments, + }; + + // Send message to server to initiate Round 1 and get server commitments + if (getIsAborted()) { + return { success: false, err: { type: "aborted" } }; + } + + const round1Resp = await reqSignEd25519Round1( + endpoint, + { msg: [...message] }, + authToken, + ); + if (round1Resp.success === false) { + return { + success: false, + err: { type: "error", msg: round1Resp.msg }, + }; + } + + const { session_id: sessionId, commitments_0: serverCommitment } = + round1Resp.data; + + // Combine and sort commitments by identifier + const allCommitments: CommitmentEntry[] = [ + clientCommitment, + serverCommitment, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Round 2: Send client commitments to server and get server signature share + if (getIsAborted()) { + return { success: false, err: { type: "aborted" } }; + } + + const round2Resp = await reqSignEd25519Round2( + endpoint, + { + session_id: sessionId, + commitments_1: clientCommitment, + }, + authToken, + ); + if (round2Resp.success === false) { + return { + success: false, + err: { type: "error", msg: round2Resp.msg }, + }; + } + + const serverSignatureShare: SignatureShareEntry = + round2Resp.data.signature_share_0; + + // Generate client signature share locally + if (getIsAborted()) { + return { success: false, err: { type: "aborted" } }; + } + + const round2Result = teddsaSignRound2( + message, + keyPackage, + new Uint8Array(round1Result.data.nonces), + allCommitments, + ); + if (!round2Result.success) { + return { success: false, err: { type: "error", msg: round2Result.err } }; + } + + const clientSignatureShare: SignatureShareEntry = { + identifier: round2Result.data.identifier, + signature_share: round2Result.data.signature_share, + }; + + // Combine and sort signature shares by identifier + const allSignatureShares: SignatureShareEntry[] = [ + clientSignatureShare, + serverSignatureShare, + ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // Aggregate: Send all data to server to get final signature + if (getIsAborted()) { + return { success: false, err: { type: "aborted" } }; + } + + // Extract user_verifying_share from keyPackage + const userVerifyingShare = keyPackage.verifying_share; + + const aggregateResp = await reqSignEd25519Aggregate( + endpoint, + { + session_id: sessionId, + msg: [...message], + all_commitments: allCommitments, + all_signature_shares: allSignatureShares, + user_verifying_share: userVerifyingShare, + }, + authToken, + ); + if (aggregateResp.success === false) { + return { + success: false, + err: { type: "error", msg: aggregateResp.msg }, + }; + } + + const signature = new Uint8Array(aggregateResp.data.signature); + + return { success: true, data: signature }; +} + export async function runTeddsaSignLocal( message: Uint8Array, keygen1: TeddsaKeygenOutputBytes, diff --git a/crypto/teddsa/teddsa_interface/tsconfig.json b/crypto/teddsa/teddsa_interface/tsconfig.json index 8d81c3fe1..e2b46401a 100644 --- a/crypto/teddsa/teddsa_interface/tsconfig.json +++ b/crypto/teddsa/teddsa_interface/tsconfig.json @@ -10,8 +10,8 @@ "declaration": true, "declarationMap": true, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/tests"], } diff --git a/embed/oko_attached/src/crypto/scalar.ts b/embed/oko_attached/src/crypto/scalar.ts new file mode 100644 index 000000000..752a1d85b --- /dev/null +++ b/embed/oko_attached/src/crypto/scalar.ts @@ -0,0 +1,35 @@ +import type { Bytes32 } from "@oko-wallet/bytes"; +import { Bytes } from "@oko-wallet/bytes"; +import { wasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm"; + +interface TeddsaScalarBaseMultInput { + scalar: number[]; +} + +interface TeddsaScalarBaseMultOutput { + point: number[]; +} + +/** + * Compute verifying_share from signing_share. + * Ed25519: verifying_share = signing_share * G (base point) + * + * Used to reconstruct KeyPackage from SSS-recovered signing_share. + * + * @param signingShare - 32-byte signing_share + * @returns 32-byte verifying_share (compressed Ed25519 point) + */ +export function computeVerifyingShare(signingShare: Bytes32): Bytes32 { + const input: TeddsaScalarBaseMultInput = { + scalar: Array.from(signingShare.toUint8Array()), + }; + + const output: TeddsaScalarBaseMultOutput = wasmModule.scalar_base_mult(input); + + const result = Bytes.fromUint8Array(new Uint8Array(output.point), 32); + if (!result.success) { + throw new Error(`Invalid verifying_share output: ${result.err}`); + } + + return result.data; +} diff --git a/embed/oko_attached/src/crypto/sign_ed25519.ts b/embed/oko_attached/src/crypto/sign_ed25519.ts index 486991b65..be595f226 100644 --- a/embed/oko_attached/src/crypto/sign_ed25519.ts +++ b/embed/oko_attached/src/crypto/sign_ed25519.ts @@ -11,7 +11,6 @@ import type { } from "@oko-wallet/teddsa-interface"; import type { Result } from "@oko-wallet/stdlib-js"; import type { MakeSignOutputError } from "@oko-wallet/oko-sdk-core"; -import { reqPresignEd25519, reqSignEd25519 } from "@oko-wallet/teddsa-api-lib"; import { TSS_V1_ENDPOINT } from "@oko-wallet-attached/requests/oko_api"; @@ -29,131 +28,133 @@ export async function makeSignOutputEd25519( getIsAborted: () => boolean, ): Promise> { try { - if (getIsAborted()) { - return { success: false, err: { type: "aborted" } }; - } - - const presignRes = await reqPresignEd25519( - TSS_V1_ENDPOINT, - {}, - apiKey, - authToken, - ); - - if (!presignRes.success) { - return { - success: false, - err: { - type: "sign_fail", - error: { type: "error", msg: presignRes.msg }, - }, - }; - } - - const { session_id: sessionId, commitments_0: serverCommitment } = - presignRes.data; - - if (getIsAborted()) { - return { success: false, err: { type: "aborted" } }; - } - - const round1Result = teddsaSignRound1(keyPackage.keyPackage); - if (!round1Result.success) { - return { - success: false, - err: { - type: "sign_fail", - error: { type: "error", msg: round1Result.err }, - }, - }; - } - - const clientCommitment: CommitmentEntry = { - identifier: round1Result.data.identifier, - commitments: round1Result.data.commitments, - }; - - const allCommitments: CommitmentEntry[] = [ - clientCommitment, - serverCommitment, - ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - - if (getIsAborted()) { - return { success: false, err: { type: "aborted" } }; - } - - const serverSignRes = await reqSignEd25519( - TSS_V1_ENDPOINT, - { - session_id: sessionId, - msg: [...message], - commitments_1: clientCommitment, - }, - authToken, - ); - - if (!serverSignRes.success) { - return { - success: false, - err: { - type: "sign_fail", - error: { type: "error", msg: serverSignRes.msg }, - }, - }; - } - - const serverSignatureShare: SignatureShareEntry = - serverSignRes.data.signature_share_0; - - if (getIsAborted()) { - return { success: false, err: { type: "aborted" } }; - } - - const round2Result = teddsaSignRound2( - message, - keyPackage.keyPackage, - new Uint8Array(round1Result.data.nonces), - allCommitments, - ); - - if (!round2Result.success) { - return { - success: false, - err: { - type: "sign_fail", - error: { type: "error", msg: round2Result.err }, - }, - }; - } - - const clientSignatureShare: SignatureShareEntry = { - identifier: round2Result.data.identifier, - signature_share: round2Result.data.signature_share, - }; - - const allSignatureShares: SignatureShareEntry[] = [ - clientSignatureShare, - serverSignatureShare, - ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); - - const aggregateResult = teddsaAggregate( - message, - allCommitments, - allSignatureShares, - keyPackage.publicKeyPackage, - ); - - if (!aggregateResult.success) { - return { - success: false, - err: { - type: "sign_fail", - error: { type: "error", msg: aggregateResult.err }, - }, - }; - } - - return { success: true, data: aggregateResult.data }; + // @TODO + + // if (getIsAborted()) { + // return { success: false, err: { type: "aborted" } }; + // } + + // const presignRes = await reqPresignEd25519( + // TSS_V1_ENDPOINT, + // {}, + // apiKey, + // authToken, + // ); + + // if (!presignRes.success) { + // return { + // success: false, + // err: { + // type: "sign_fail", + // error: { type: "error", msg: presignRes.msg }, + // }, + // }; + // } + + // const { session_id: sessionId, commitments_0: serverCommitment } = + // presignRes.data; + + // if (getIsAborted()) { + // return { success: false, err: { type: "aborted" } }; + // } + + // const round1Result = teddsaSignRound1(keyPackage.keyPackage); + // if (!round1Result.success) { + // return { + // success: false, + // err: { + // type: "sign_fail", + // error: { type: "error", msg: round1Result.err }, + // }, + // }; + // } + + // const clientCommitment: CommitmentEntry = { + // identifier: round1Result.data.identifier, + // commitments: round1Result.data.commitments, + // }; + + // const allCommitments: CommitmentEntry[] = [ + // clientCommitment, + // serverCommitment, + // ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // if (getIsAborted()) { + // return { success: false, err: { type: "aborted" } }; + // } + + // const serverSignRes = await reqSignEd25519( + // TSS_V1_ENDPOINT, + // { + // session_id: sessionId, + // msg: [...message], + // commitments_1: clientCommitment, + // }, + // authToken, + // ); + + // if (!serverSignRes.success) { + // return { + // success: false, + // err: { + // type: "sign_fail", + // error: { type: "error", msg: serverSignRes.msg }, + // }, + // }; + // } + + // const serverSignatureShare: SignatureShareEntry = + // serverSignRes.data.signature_share_0; + + // if (getIsAborted()) { + // return { success: false, err: { type: "aborted" } }; + // } + + // const round2Result = teddsaSignRound2( + // message, + // keyPackage.keyPackage, + // new Uint8Array(round1Result.data.nonces), + // allCommitments, + // ); + + // if (!round2Result.success) { + // return { + // success: false, + // err: { + // type: "sign_fail", + // error: { type: "error", msg: round2Result.err }, + // }, + // }; + // } + + // const clientSignatureShare: SignatureShareEntry = { + // identifier: round2Result.data.identifier, + // signature_share: round2Result.data.signature_share, + // }; + + // const allSignatureShares: SignatureShareEntry[] = [ + // clientSignatureShare, + // serverSignatureShare, + // ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0)); + + // const aggregateResult = teddsaAggregate( + // message, + // allCommitments, + // allSignatureShares, + // keyPackage.publicKeyPackage, + // ); + + // if (!aggregateResult.success) { + // return { + // success: false, + // err: { + // type: "sign_fail", + // error: { type: "error", msg: aggregateResult.err }, + // }, + // }; + // } + + return { success: true, data: new Uint8Array(64) }; } catch (error) { return { success: false, diff --git a/embed/oko_attached/src/crypto/sss_ed25519.ts b/embed/oko_attached/src/crypto/sss_ed25519.ts index d1c0b9476..50d375fad 100644 --- a/embed/oko_attached/src/crypto/sss_ed25519.ts +++ b/embed/oko_attached/src/crypto/sss_ed25519.ts @@ -1,241 +1,187 @@ -// refactor this file @chemonoworld - -// import type { KeyShareNodeMetaWithNodeStatusInfo } from "@oko-wallet/oko-types/tss"; -// import { wasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm"; -// import { Bytes, type Bytes32 } from "@oko-wallet/bytes"; -// import type { -// PointNumArr, -// UserKeySharePointByNode, -// } from "@oko-wallet/oko-types/user_key_share"; -// import type { TeddsaKeygenOutputBytes } from "@oko-wallet/teddsa-hooks"; -// import type { Result } from "@oko-wallet/stdlib-js"; - -// import { hashKeyshareNodeNamesEd25519 } from "./hash"; - -// /** -// * Data structure for Ed25519 key share backup on KS nodes. -// * Contains the SSS split of signing_share along with public info needed for reconstruction. -// */ -// export interface Ed25519KeyShareBackup { -// /** SSS split point (x: identifier, y: share) */ -// share: { -// x: Bytes32; -// y: Bytes32; -// }; -// /** Serialized PublicKeyPackage (needed for reconstruction) */ -// publicKeyPackage: string; // hex string -// /** Participant identifier (needed for reconstruction) */ -// identifier: string; // hex string -// /** Ed25519 public key (for verification) */ -// publicKey: string; // hex string -// } - -// /** -// * Split Ed25519 key package for backup on Key Share Nodes. -// * -// * Extracts the signing_share from the key_package and splits it using SSS. -// * Also includes the public information needed to reconstruct the key_package. -// */ -// export async function splitUserKeySharesEd25519( -// keygen_1: TeddsaKeygenOutputBytes, -// keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo, -// ): Promise> { -// try { -// const keyPackageBytes = [...keygen_1.key_package]; - -// // Extract signing_share from key_package (32-byte scalar) -// const signingShareArr: number[] = -// wasmModule.extract_signing_share(keyPackageBytes); - -// // Hash KS node names to get identifiers for SSS (Ed25519-compatible) -// const keyshareNodeHashesRes = await hashKeyshareNodeNamesEd25519( -// keyshareNodeMeta.nodes.map((meta) => meta.name), -// ); -// if (keyshareNodeHashesRes.success === false) { -// return { -// success: false, -// err: keyshareNodeHashesRes.err, -// }; -// } -// const keyshareNodeHashes = keyshareNodeHashesRes.data.map((bytes) => { -// return [...bytes.toUint8Array()]; -// }); - -// // Split signing_share using SSS -// const splitPoints: PointNumArr[] = wasmModule.sss_split( -// signingShareArr, -// keyshareNodeHashes, -// keyshareNodeMeta.threshold, -// ); - -// // Convert to UserKeySharePointByNode format -// const shares: UserKeySharePointByNode[] = splitPoints.map( -// (point: PointNumArr, index: number) => { -// const xBytesRes = Bytes.fromUint8Array( -// Uint8Array.from([...point.x]), -// 32, -// ); -// if (xBytesRes.success === false) { -// throw new Error(xBytesRes.err); -// } -// const yBytesRes = Bytes.fromUint8Array( -// Uint8Array.from([...point.y]), -// 32, -// ); -// if (yBytesRes.success === false) { -// throw new Error(yBytesRes.err); -// } -// return { -// node: { -// name: keyshareNodeMeta.nodes[index].name, -// endpoint: keyshareNodeMeta.nodes[index].endpoint, -// }, -// share: { -// x: xBytesRes.data, -// y: yBytesRes.data, -// }, -// }; -// }, -// ); - -// return { -// success: true, -// data: shares, -// }; -// } catch (error: any) { -// return { -// success: false, -// err: `splitUserKeySharesEd25519 failed: ${String(error)}`, -// }; -// } -// } - -// /** -// * Combine Ed25519 key shares to recover the signing_share. -// */ -// export async function combineUserSharesEd25519( -// userKeySharePoints: UserKeySharePointByNode[], -// threshold: number, -// ): Promise> { -// try { -// if (threshold < 2) { -// return { -// success: false, -// err: "Threshold must be at least 2", -// }; -// } - -// if (userKeySharePoints.length < threshold) { -// return { -// success: false, -// err: "Number of user key shares is less than threshold", -// }; -// } - -// const points: PointNumArr[] = userKeySharePoints.map( -// (userKeySharePoint) => ({ -// x: [...userKeySharePoint.share.x.toUint8Array()], -// y: [...userKeySharePoint.share.y.toUint8Array()], -// }), -// ); - -// // Combine shares to recover signing_share -// const combinedSigningShare: number[] = wasmModule.sss_combine( -// points, -// threshold, -// ); - -// return { -// success: true, -// data: Uint8Array.from(combinedSigningShare), -// }; -// } catch (e) { -// return { -// success: false, -// err: `combineUserSharesEd25519 failed: ${String(e)}`, -// }; -// } -// } - -// /** -// * Reconstruct a full KeyPackage from a recovered signing_share and public info. -// */ -// export async function reconstructKeyPackageEd25519( -// signingShare: Uint8Array, -// publicKeyPackage: Uint8Array, -// identifier: Uint8Array, -// ): Promise> { -// try { -// const keyPackageArr: number[] = wasmModule.reconstruct_key_package( -// [...signingShare], -// [...publicKeyPackage], -// [...identifier], -// ); - -// return { -// success: true, -// data: Uint8Array.from(keyPackageArr), -// }; -// } catch (e) { -// return { -// success: false, -// err: `reconstructKeyPackageEd25519 failed: ${String(e)}`, -// }; -// } -// } - -// /** -// * Full recovery of Ed25519 keygen output from KS node shares. -// * -// * Given the SSS shares from KS nodes plus the stored public info, -// * reconstructs the complete TeddsaKeygenOutputBytes. -// */ -// export async function recoverEd25519Keygen( -// userKeySharePoints: UserKeySharePointByNode[], -// threshold: number, -// publicKeyPackage: Uint8Array, -// identifier: Uint8Array, -// publicKey: Bytes32, -// ): Promise> { -// try { -// // Combine shares to recover signing_share -// const combineRes = await combineUserSharesEd25519( -// userKeySharePoints, -// threshold, -// ); -// if (!combineRes.success) { -// return { -// success: false, -// err: combineRes.err, -// }; -// } - -// // Reconstruct key_package from signing_share + public info -// const reconstructRes = await reconstructKeyPackageEd25519( -// combineRes.data, -// publicKeyPackage, -// identifier, -// ); -// if (!reconstructRes.success) { -// return { -// success: false, -// err: reconstructRes.err, -// }; -// } - -// return { -// success: true, -// data: { -// key_package: reconstructRes.data, -// public_key_package: publicKeyPackage, -// identifier: identifier, -// public_key: publicKey, -// }, -// }; -// } catch (e) { -// return { -// success: false, -// err: `recoverEd25519Keygen failed: ${String(e)}`, -// }; -// } -// } +import type { KeyShareNodeMetaWithNodeStatusInfo } from "@oko-wallet/oko-types/tss"; +import { wasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm"; +import { Bytes, type Bytes32 } from "@oko-wallet/bytes"; +import type { TeddsaKeyShareByNode } from "@oko-wallet/oko-types/user_key_share"; +import type { Result } from "@oko-wallet/stdlib-js"; +import type { + KeyPackage, + KeyPackageRaw, + PublicKeyPackageRaw, +} from "@oko-wallet/teddsa-interface"; + +import { hashKeyshareNodeNamesEd25519 } from "./hash"; +import { computeVerifyingShare } from "./scalar"; + +interface SplitOutputRaw { + key_packages: KeyPackageRaw[]; + public_key_package: PublicKeyPackageRaw; +} + +/** + * Split client signing_share using SSS for KS node distribution. + * + * Uses FROST's sss_split to split the signing_share. + * Extracts identifier and signing_share from each KeyPackage. + * + * @param signingShare - FROST KeyPackage's signing_share (32 bytes) + * @param keyshareNodeMeta - KS Node metadata (nodes, threshold) + * @returns TeddsaKeyShareByNode[] - (identifier, signing_share) pairs per node + */ +export async function splitTeddsaSigningShare( + signingShare: Bytes32, + keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo, +): Promise> { + try { + const signingShareArr = [...signingShare.toUint8Array()]; + + const identifiersRes = await hashKeyshareNodeNamesEd25519( + keyshareNodeMeta.nodes.map((n) => n.name), + ); + if (!identifiersRes.success) { + return { success: false, err: identifiersRes.err }; + } + + const identifiers = identifiersRes.data.map((b) => [...b.toUint8Array()]); + + const splitOutput: SplitOutputRaw = wasmModule.sss_split( + signingShareArr, + identifiers, + keyshareNodeMeta.threshold, + ); + + const shares: TeddsaKeyShareByNode[] = splitOutput.key_packages.map( + (kp, i) => { + const idBytes = Bytes.fromUint8Array( + new Uint8Array(kp.identifier), + 32, + ); + const shareBytes = Bytes.fromUint8Array( + new Uint8Array(kp.signing_share), + 32, + ); + + if (!idBytes.success) throw new Error(idBytes.err); + if (!shareBytes.success) throw new Error(shareBytes.err); + + return { + node: { + name: keyshareNodeMeta.nodes[i].name, + endpoint: keyshareNodeMeta.nodes[i].endpoint, + }, + share: { + identifier: idBytes.data, + signing_share: shareBytes.data, + }, + }; + }, + ); + + return { success: true, data: shares }; + } catch (e) { + return { + success: false, + err: `splitTeddsaSigningShare failed: ${String(e)}`, + }; + } +} + +/** + * Combine shares from KS nodes to recover signing_share. + * + * Converts TeddsaKeyShares to KeyPackageRaw format for sss_combine. + * + * @param shares - TeddsaKeyShare array from KS nodes + * @param threshold - SSS threshold (minimum shares required) + * @param verifyingKey - PublicKeyPackage's verifying_key (needed for KeyPackageRaw) + * @returns signing_share (32 bytes) + */ +export async function combineTeddsaShares( + shares: TeddsaKeyShareByNode[], + threshold: number, + verifyingKey: Bytes32, +): Promise> { + try { + if (shares.length < threshold) { + return { + success: false, + err: `Not enough shares: got ${shares.length}, need ${threshold}`, + }; + } + + const keyPackages: KeyPackageRaw[] = shares.map((s) => { + const verifyingShare = computeVerifyingShare(s.share.signing_share); + + return { + identifier: [...s.share.identifier.toUint8Array()], + signing_share: [...s.share.signing_share.toUint8Array()], + verifying_share: [...verifyingShare.toUint8Array()], + verifying_key: [...verifyingKey.toUint8Array()], + min_signers: threshold, + }; + }); + + const combined: number[] = wasmModule.sss_combine(keyPackages); + + const result = Bytes.fromUint8Array(Uint8Array.from(combined), 32); + if (!result.success) { + return { success: false, err: result.err }; + } + + return { success: true, data: result.data }; + } catch (e) { + return { success: false, err: `combineTeddsaShares failed: ${String(e)}` }; + } +} + +/** + * Reconstruct KeyPackage from recovered signing_share. + * + * Computes verifying_share from signing_share to create complete KeyPackage. + * + * @param signingShare - SSS-recovered client signing_share + * @param frostIdentifier - FROST P0 identifier (client is always 1) + * @param verifyingKey - PublicKeyPackage's verifying_key + * @param minSigners - threshold (2 for 2-of-2) + * @returns Complete FROST KeyPackage + */ +export function reconstructKeyPackage( + signingShare: Bytes32, + frostIdentifier: Bytes32, + verifyingKey: Bytes32, + minSigners: number, +): KeyPackage { + const verifyingShare = computeVerifyingShare(signingShare); + + return { + identifier: frostIdentifier, + signing_share: signingShare, + verifying_share: verifyingShare, + verifying_key: verifyingKey, + min_signers: minSigners, + }; +} + +/** + * Extract signing_share from KeyPackageRaw. + */ +export function extractSigningShare( + keyPackage: KeyPackageRaw, +): Result { + const signingShareRes = Bytes.fromUint8Array( + new Uint8Array(keyPackage.signing_share), + 32, + ); + if (!signingShareRes.success) { + return { success: false, err: signingShareRes.err }; + } + return { success: true, data: signingShareRes.data }; +} + +/** + * Extract signing_share from KeyPackage (Bytes type). + */ +export function extractSigningShareFromKeyPackage( + keyPackage: KeyPackage, +): Bytes32 { + return keyPackage.signing_share; +} diff --git a/examples/cosmoskit_nextjs/app/page.tsx b/examples/cosmoskit_nextjs/app/page.tsx index 789a79b51..e2d5bca69 100644 --- a/examples/cosmoskit_nextjs/app/page.tsx +++ b/examples/cosmoskit_nextjs/app/page.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ChainProvider } from "@cosmos-kit/react"; -import { makeOkoWallets } from "@oko-wallet/oko-cosmos-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-cosmos-kit"; import { chains, assets } from "chain-registry"; import App from "@/components/App"; import "@interchain-ui/react/styles"; @@ -10,7 +10,7 @@ import "@interchain-ui/react/styles"; const queryClient = new QueryClient(); export default function Home() { - const okoWallets = makeOkoWallets({ + const okoWallet = makeOkoWallet({ apiKey: process.env.NEXT_PUBLIC_OKO_API_KEY!, }); return ( @@ -18,7 +18,7 @@ export default function Home() { diff --git a/examples/evm_wagmi_nextjs/components/ConnectWalletButton.tsx b/examples/evm_wagmi_nextjs/components/ConnectWalletButton.tsx index b03b2f975..6db7f14de 100644 --- a/examples/evm_wagmi_nextjs/components/ConnectWalletButton.tsx +++ b/examples/evm_wagmi_nextjs/components/ConnectWalletButton.tsx @@ -12,7 +12,7 @@ export default function ConnectWalletButton() { chain, openAccountModal, openChainModal, - openConnectModal, + openSignInModal, authenticationStatus, mounted, }) => { @@ -33,7 +33,7 @@ export default function ConnectWalletButton() { {(() => { if (!connected) { return ( - ); diff --git a/examples/interchainkit_nextjs/app/page.tsx b/examples/interchainkit_nextjs/app/page.tsx index f9fcfebba..e036250c6 100644 --- a/examples/interchainkit_nextjs/app/page.tsx +++ b/examples/interchainkit_nextjs/app/page.tsx @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ChainProvider, InterchainWalletModal } from "@interchain-kit/react"; import { chains, assetLists } from "@chain-registry/v2"; import App from "@/components/App"; -import { makeOkoWallets } from "@oko-wallet/oko-interchain-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-interchain-kit"; import "@interchain-kit/react/styles.css"; const queryClient = new QueryClient(); @@ -18,20 +18,20 @@ const filteredAssetLists = assetLists.filter( ); export default function Home() { - const okoWallets = + const okoWallet = typeof window !== "undefined" - ? makeOkoWallets({ + ? makeOkoWallet({ apiKey: process.env.NEXT_PUBLIC_OKO_API_KEY!, sdkEndpoint: process.env.NEXT_PUBLIC_OKO_SDK_ENDPOINT, }) - : []; + : null; return ( } > diff --git a/internals/ci/src/cmds/build_frost.ts b/internals/ci/src/cmds/build_frost.ts index 9aea276b8..d8ea8c02d 100644 --- a/internals/ci/src/cmds/build_frost.ts +++ b/internals/ci/src/cmds/build_frost.ts @@ -14,11 +14,10 @@ export async function buildFrost(..._args: any[]) { expectSuccess(wasmRet, "wasm build failed"); console.log("%s %s", chalk.bold.green("Done"), "build wasm frost keplr"); - // TODO: copy wasm to oko_attached or other target locations @chemonoworld - // const copyRet = spawnSync("yarn", ["run", "copy_wasm"], { - // cwd: paths.oko_attached, - // stdio: "inherit", - // }); - // expectSuccess(copyRet, "copy failed"); - // console.log("%s %s", chalk.bold.green("Done"), "copy wasm"); + const copyRet = spawnSync("yarn", ["run", "copy_wasm"], { + cwd: paths.oko_attached, + stdio: "inherit", + }); + expectSuccess(copyRet, "copy failed"); + console.log("%s %s", chalk.bold.green("Done"), "copy wasm"); } diff --git a/key_share_node/ksn_interface/src/key_share.ts b/key_share_node/ksn_interface/src/key_share.ts index 88b3e2d1a..e827295ed 100644 --- a/key_share_node/ksn_interface/src/key_share.ts +++ b/key_share_node/ksn_interface/src/key_share.ts @@ -27,6 +27,21 @@ export type UpdateKeyShareRequest = { status: KeyShareStatus; }; +/** + * Key share registration request for KS node. + * + * Share format by curve_type (both are 64 bytes): + * + * secp256k1: + * Point256 { x: 32 bytes, y: 32 bytes } + * - Elliptic curve point coordinates + * + * ed25519 (TEDDSA): + * TeddsaKeyShare { identifier: 32 bytes, signing_share: 32 bytes } + * - identifier: SSS x-coordinate (node_name SHA256 hash, byte[31] &= 0x0F) + * - signing_share: SSS y-coordinate (split signing share) + * - Note: verifying_share is recovered from signing_share via scalar_base_mult + */ export interface RegisterKeyShareRequest { user_auth_id: string; auth_type: AuthType; @@ -76,6 +91,12 @@ export interface CheckKeyShareRequestBody { public_key: string; // hex string } +/** + * Key share reshare request for KS node. + * + * Share format is same as RegisterKeyShareRequest (64 bytes). + * See RegisterKeyShareRequest for detailed format by curve_type. + */ export interface ReshareKeyShareRequest { user_auth_id: string; auth_type: AuthType; diff --git a/sandbox/sandbox_cosmos_kit/app/page.tsx b/sandbox/sandbox_cosmos_kit/app/page.tsx index 789a79b51..e2d5bca69 100644 --- a/sandbox/sandbox_cosmos_kit/app/page.tsx +++ b/sandbox/sandbox_cosmos_kit/app/page.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ChainProvider } from "@cosmos-kit/react"; -import { makeOkoWallets } from "@oko-wallet/oko-cosmos-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-cosmos-kit"; import { chains, assets } from "chain-registry"; import App from "@/components/App"; import "@interchain-ui/react/styles"; @@ -10,7 +10,7 @@ import "@interchain-ui/react/styles"; const queryClient = new QueryClient(); export default function Home() { - const okoWallets = makeOkoWallets({ + const okoWallet = makeOkoWallet({ apiKey: process.env.NEXT_PUBLIC_OKO_API_KEY!, }); return ( @@ -18,7 +18,7 @@ export default function Home() { diff --git a/sandbox/sandbox_evm/src/services/web3/wagmiConfig.tsx b/sandbox/sandbox_evm/src/services/web3/wagmiConfig.tsx index 9f1d9f363..7150c95d7 100644 --- a/sandbox/sandbox_evm/src/services/web3/wagmiConfig.tsx +++ b/sandbox/sandbox_evm/src/services/web3/wagmiConfig.tsx @@ -130,9 +130,9 @@ function okoConnector( // popup on safari works fine here as we use cached states console.log( - "[sandbox-evm] no authenticated account, sign in with google", + "[sandbox-evm] no authenticated account, opening connect modal", ); - await okoEthWallet.okoWallet.signIn("google"); + await okoEthWallet.okoWallet.openSignInModal(); } const chainId = await wallet.getChainId(); diff --git a/sandbox/sandbox_interchain_kit/app/page.tsx b/sandbox/sandbox_interchain_kit/app/page.tsx index 802d7e5f5..8cc60e2ee 100644 --- a/sandbox/sandbox_interchain_kit/app/page.tsx +++ b/sandbox/sandbox_interchain_kit/app/page.tsx @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ChainProvider, InterchainWalletModal } from "@interchain-kit/react"; import { chains, assetLists } from "@chain-registry/v2"; import App from "@/components/App"; -import { makeOkoWallets } from "@oko-wallet/oko-interchain-kit"; +import { makeOkoWallet } from "@oko-wallet/oko-interchain-kit"; import "@interchain-kit/react/styles.css"; const queryClient = new QueryClient(); @@ -16,20 +16,20 @@ const filteredAssetLists = assetLists.filter( ); export default function Home() { - const okoWallets = + const okoWallet = typeof window !== "undefined" - ? makeOkoWallets({ + ? makeOkoWallet({ apiKey: process.env.NEXT_PUBLIC_OKO_API_KEY!, sdkEndpoint: process.env.NEXT_PUBLIC_OKO_SDK_ENDPOINT, }) - : []; + : null; return ( } > diff --git a/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts b/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts index 291fca00d..40a3ed835 100644 --- a/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts +++ b/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts @@ -98,8 +98,8 @@ export function useOkoSol() { // Check if user is signed in const existingPubkey = await okoSolWallet.okoWallet.getPublicKey(); if (!existingPubkey) { - // Not signed in - trigger OAuth sign in - await okoSolWallet.okoWallet.signIn("google"); + // Not signed in - open provider select modal + await okoSolWallet.okoWallet.openSignInModal(); } // connect() internally handles Ed25519 key creation if needed diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..c24d55c53 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,125 @@ +# Scripts + +Shell scripts for local development environment setup and E2E testing. + +## Prerequisites + +- Node.js 22+ +- tmux +- Rust (optional, for cargo check) + +## Scripts + +### ci-setup.sh + +Sets up the local development environment by running the same build steps as GitHub CI. + +```bash +./scripts/ci-setup.sh [OPTIONS] +``` + +**Build Steps:** +1. Check Node.js version (22+) +2. Enable corepack +3. Install dependencies (`yarn install --immutable`) +4. Build CS packages (cait-sith WASM) +5. Build Frost packages (frost-ed25519 WASM) +6. Build internal packages +7. Build SDK packages +8. Run TypeScript typecheck +9. Run Rust cargo check + +**Options:** + +| Option | Description | +|--------|-------------| +| `--skip-rust` | Skip Rust cargo check | +| `--skip-typecheck` | Skip TypeScript typecheck | +| `-h, --help` | Show help message | + +**Examples:** + +```bash +# Full setup +./scripts/ci-setup.sh + +# Quick setup (skip checks) +./scripts/ci-setup.sh --skip-rust --skip-typecheck +``` + +--- + +### tmux-e2e-start.sh + +Starts all E2E services in a tmux session with separate windows. + +```bash +./scripts/tmux-e2e-start.sh [OPTIONS] +``` + +**Services Started:** + +| Window | Service | Command | +|--------|---------|---------| +| oko_api | Backend API | `yarn dev` | +| oko_attached | Embedded wallet | `yarn dev` | +| demo_web | Demo web app | `yarn dev` | +| ksn_1 | Key Share Node 1 | `yarn start` | +| ksn_2 | Key Share Node 2 | `yarn start_2` | +| ksn_3 | Key Share Node 3 | `yarn start_3` | + +**Options:** + +| Option | Description | +|--------|-------------| +| `--reset` | Reset database before starting (runs migrations and seed) | +| `-h, --help` | Show help message | + +**Database Reset Steps (when using `--reset`):** +1. `yarn ci db_migrate_api --use-env-file` +2. `yarn ci db_seed_api --use-env-file` +3. `yarn ci db_migrate_ksn --use-env-file` + +**Examples:** + +```bash +# Start services +./scripts/tmux-e2e-start.sh + +# Reset database and start +./scripts/tmux-e2e-start.sh --reset +``` + +--- + +### tmux-e2e-stop.sh + +Stops the E2E tmux session. + +```bash +./scripts/tmux-e2e-stop.sh +``` + +--- + +## Typical Workflow + +```bash +# 1. First time setup (run once) +./scripts/ci-setup.sh + +# 2. Start E2E environment with fresh database +./scripts/tmux-e2e-start.sh --reset + +# 3. When done, stop all services +./scripts/tmux-e2e-stop.sh +``` + +## Tmux Navigation + +Once attached to the session: +- `Ctrl+b n` - Next window +- `Ctrl+b p` - Previous window +- `Ctrl+b ` - Go to window by number (0-5) +- `Ctrl+b d` - Detach from session (services keep running) +- `Ctrl+b w` - List all windows diff --git a/scripts/ci-setup.sh b/scripts/ci-setup.sh new file mode 100755 index 000000000..e2fa6f7e6 --- /dev/null +++ b/scripts/ci-setup.sh @@ -0,0 +1,136 @@ +#!/bin/bash +set -e + +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_ROOT" + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +print_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Parse options +SKIP_RUST=false +SKIP_TYPECHECK=false + +while [[ $# -gt 0 ]]; do + case $1 in + --skip-rust) + SKIP_RUST=true + shift + ;; + --skip-typecheck) + SKIP_TYPECHECK=true + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Local CI setup script - prepares the development environment" + echo "" + echo "Options:" + echo " --skip-rust Skip Rust cargo check" + echo " --skip-typecheck Skip TypeScript typecheck" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + echo "Use -h or --help for usage information" + exit 1 + ;; + esac +done + +echo "" +echo "==========================================" +echo " Oko Local CI Setup" +echo "==========================================" +echo "" + +# Check Node.js version +print_step "Checking Node.js version..." +NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt 22 ]; then + print_error "Node.js 22+ required. Current: $(node -v)" + exit 1 +fi +print_info "Node.js $(node -v) OK" + +# Enable corepack +print_step "Enabling corepack..." +corepack enable +print_info "Corepack enabled" + +# Install dependencies +print_step "Installing dependencies..." +yarn install --immutable +print_info "Dependencies installed" + +# Build CS packages (WASM) +print_step "Building CS packages (cait-sith WASM)..." +yarn ci build_cs +print_info "CS packages built" + +# Build Frost packages (EdDSA WASM) +print_step "Building Frost packages (frost-ed25519 WASM)..." +yarn ci build_frost +print_info "Frost packages built" + +# Build internal packages +print_step "Building internal packages..." +yarn ci build_pkgs +print_info "Internal packages built" + +# Build SDK packages +print_step "Building SDK packages..." +yarn ci build_sdk +print_info "SDK packages built" + +# TypeScript typecheck +if [ "$SKIP_TYPECHECK" = false ]; then + print_step "Running TypeScript typecheck..." + yarn ci typecheck + print_info "Typecheck passed" +else + print_warn "Skipping TypeScript typecheck" +fi + +# Rust check +if [ "$SKIP_RUST" = false ]; then + if command -v cargo &> /dev/null; then + print_step "Running Rust cargo check..." + cargo check --workspace + print_info "Rust check passed" + else + print_warn "Rust not installed, skipping cargo check" + fi +else + print_warn "Skipping Rust cargo check" +fi + +echo "" +echo "==========================================" +echo -e " ${GREEN}Setup Complete!${NC}" +echo "==========================================" +echo "" +print_info "You can now run: ./scripts/tmux-e2e-start.sh" +echo "" diff --git a/scripts/tmux-e2e-start.sh b/scripts/tmux-e2e-start.sh new file mode 100755 index 000000000..01126ddfb --- /dev/null +++ b/scripts/tmux-e2e-start.sh @@ -0,0 +1,113 @@ +#!/bin/bash +set -e + +SESSION_NAME="oko-e2e" +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +RESET_DB=false + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Parse options +while [[ $# -gt 0 ]]; do + case $1 in + --reset) + RESET_DB=true + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --reset Reset database before starting services" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + echo "Use -h or --help for usage information" + exit 1 + ;; + esac +done + +# Check tmux installation +if ! command -v tmux &> /dev/null; then + print_error "tmux is not installed. Please install tmux first." + exit 1 +fi + +# Check and kill existing session +if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + print_warn "Existing session '$SESSION_NAME' found. Killing it..." + tmux kill-session -t "$SESSION_NAME" +fi + +# Reset database +if [ "$RESET_DB" = true ]; then + print_info "Resetting database..." + cd "$PROJECT_ROOT" + + print_info "Running: yarn ci db_migrate_api --use-env-file" + yarn ci db_migrate_api --use-env-file + + print_info "Running: yarn ci db_seed_api --use-env-file" + yarn ci db_seed_api --use-env-file + + print_info "Running: yarn ci db_migrate_ksn --use-env-file" + yarn ci db_migrate_ksn --use-env-file + + print_info "Database reset complete!" +fi + +print_info "Creating tmux session: $SESSION_NAME" + +# 1. oko_api (create session with first window) +tmux new-session -d -s "$SESSION_NAME" -n "oko_api" -c "$PROJECT_ROOT/backend/oko_api/server" +tmux send-keys -t "$SESSION_NAME:oko_api" "yarn dev" C-m + +# 2. oko_attached +tmux new-window -t "$SESSION_NAME" -n "oko_attached" -c "$PROJECT_ROOT/embed/oko_attached" +tmux send-keys -t "$SESSION_NAME:oko_attached" "yarn dev" C-m + +# 3. demo_web +tmux new-window -t "$SESSION_NAME" -n "demo_web" -c "$PROJECT_ROOT/apps/demo_web" +tmux send-keys -t "$SESSION_NAME:demo_web" "yarn dev" C-m + +# 4. ksn_1 +tmux new-window -t "$SESSION_NAME" -n "ksn_1" -c "$PROJECT_ROOT/key_share_node/server" +tmux send-keys -t "$SESSION_NAME:ksn_1" "yarn start" C-m + +# 5. ksn_2 +tmux new-window -t "$SESSION_NAME" -n "ksn_2" -c "$PROJECT_ROOT/key_share_node/server" +tmux send-keys -t "$SESSION_NAME:ksn_2" "yarn start_2" C-m + +# 6. ksn_3 +tmux new-window -t "$SESSION_NAME" -n "ksn_3" -c "$PROJECT_ROOT/key_share_node/server" +tmux send-keys -t "$SESSION_NAME:ksn_3" "yarn start_3" C-m + +# Select first window +tmux select-window -t "$SESSION_NAME:oko_api" + +print_info "All services started in tmux session: $SESSION_NAME" +print_info "Windows: oko_api, oko_attached, demo_web, ksn_1, ksn_2, ksn_3" +echo "" +print_info "Attaching to session..." + +# Attach to session +tmux attach-session -t "$SESSION_NAME" diff --git a/scripts/tmux-e2e-stop.sh b/scripts/tmux-e2e-stop.sh new file mode 100755 index 000000000..1bb61f87f --- /dev/null +++ b/scripts/tmux-e2e-stop.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +SESSION_NAME="oko-e2e" + +# Color definitions +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + tmux kill-session -t "$SESSION_NAME" + echo -e "${GREEN}[INFO]${NC} Session '$SESSION_NAME' terminated." +else + echo -e "${YELLOW}[WARN]${NC} Session '$SESSION_NAME' not found." +fi diff --git a/sdk/oko_cosmos_kit/src/chain-wallet.ts b/sdk/oko_cosmos_kit/src/chain-wallet.ts index f8ab63821..995456ca9 100644 --- a/sdk/oko_cosmos_kit/src/chain-wallet.ts +++ b/sdk/oko_cosmos_kit/src/chain-wallet.ts @@ -25,4 +25,5 @@ export class OkoChainWallet extends ChainWalletBase { await super.update(); } + } diff --git a/sdk/oko_cosmos_kit/src/client.ts b/sdk/oko_cosmos_kit/src/client.ts index cca3b1675..d753e3158 100644 --- a/sdk/oko_cosmos_kit/src/client.ts +++ b/sdk/oko_cosmos_kit/src/client.ts @@ -19,12 +19,10 @@ import type { WalletClient, } from "@cosmos-kit/core"; import { BroadcastMode } from "@keplr-wallet/types"; -import type { SignInType } from "@oko-wallet/oko-sdk-core"; import type { OkoCosmosWalletInterface } from "@oko-wallet/oko-sdk-cosmos"; export class OkoWalletClient implements WalletClient { readonly client: OkoCosmosWalletInterface; - readonly loginProvider: SignInType; private _defaultSignOptions: SignOptions = { preferNoSetFee: false, preferNoSetMemo: false, @@ -39,25 +37,10 @@ export class OkoWalletClient implements WalletClient { this._defaultSignOptions = options; } - constructor(client: OkoCosmosWalletInterface, loginProvider: SignInType) { + constructor(client: OkoCosmosWalletInterface) { this.client = client; - this.loginProvider = loginProvider; } - // async startEmailSignIn(email: string) { - // if (this.loginProvider !== 'email') { - // throw new Error('Email login is not enabled for this wallet instance'); - // } - // return await this.client.okoWallet.startEmailSignIn(email); - // } - - // async completeEmailSignIn(email: string, code: string) { - // if (this.loginProvider !== 'email') { - // throw new Error('Email login is not enabled for this wallet instance'); - // } - // return await this.client.okoWallet.completeEmailSignIn(email, code); - // } - async enable(_chainIds: string | string[]) {} async suggestToken(_suggestToken: SuggestToken) {} @@ -77,9 +60,8 @@ export class OkoWalletClient implements WalletClient { if (!publicKey) { try { - await this.client.okoWallet.signIn(this.loginProvider); + await this.client.okoWallet.openSignInModal(); } catch { - // Must match rejectMessage.source in registry.ts for cosmos-kit to recognize rejection throw new Error("Request rejected"); } } diff --git a/sdk/oko_cosmos_kit/src/constant.ts b/sdk/oko_cosmos_kit/src/constant.ts index 57e62b385..0dc663532 100644 --- a/sdk/oko_cosmos_kit/src/constant.ts +++ b/sdk/oko_cosmos_kit/src/constant.ts @@ -4,25 +4,3 @@ export const OKO_ICON = export const GOOGLE_LOGO = "data:image/png;base64,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"; -export const PROVIDER_CONFIG: Record = { - google: { - logo: GOOGLE_LOGO, - name: "Oko Wallet (Google)", - }, - email: { - logo: OKO_ICON, - name: "Oko Wallet (Email)", - }, - x: { - logo: OKO_ICON, - name: "Oko Wallet (X)", - }, - telegram: { - logo: OKO_ICON, - name: "Oko Wallet (Telegram)", - }, - discord: { - logo: OKO_ICON, - name: "Oko Wallet (Discord)", - }, -}; diff --git a/sdk/oko_cosmos_kit/src/index.ts b/sdk/oko_cosmos_kit/src/index.ts index 2dd33efb8..cc6c1653f 100644 --- a/sdk/oko_cosmos_kit/src/index.ts +++ b/sdk/oko_cosmos_kit/src/index.ts @@ -1,34 +1,11 @@ -import type { SignInType } from "@oko-wallet/oko-sdk-core"; - -import { PROVIDER_CONFIG } from "./constant"; import { OkoMainWallet } from "./main-wallet"; import { okoWalletInfo } from "./registry"; import type { OkoWalletOptions } from "./types"; -export const makeOkoWallets = (options: OkoWalletOptions): OkoMainWallet[] => { - // If no loginMethods specified, use all available providers - const providers = - options.loginMethods?.map((m) => m.provider) ?? - (Object.keys(PROVIDER_CONFIG) as SignInType[]); - - return providers.map((provider) => { - const providerConfig = PROVIDER_CONFIG[provider]; - - const { loginMethods, ...baseOptions } = options; - const internalOptions = { - ...baseOptions, - loginProvider: provider, - }; - - return new OkoMainWallet({ - ...okoWalletInfo, - name: okoWalletInfo.name + "_" + provider, - prettyName: providerConfig?.name || okoWalletInfo.prettyName, - logo: providerConfig?.logo - ? { major: okoWalletInfo.logo as string, minor: providerConfig.logo } - : okoWalletInfo.logo, - options: internalOptions, - }); +export const makeOkoWallet = (options: OkoWalletOptions): OkoMainWallet => { + return new OkoMainWallet({ + ...okoWalletInfo, + options, }); }; @@ -36,4 +13,4 @@ export { OkoChainWallet } from "./chain-wallet"; export { OkoWalletClient } from "./client"; export { OkoMainWallet } from "./main-wallet"; export { okoWalletInfo } from "./registry"; -export type { OkoLoginMethod, OkoWalletOptions } from "./types"; +export type { OkoWalletOptions } from "./types"; diff --git a/sdk/oko_cosmos_kit/src/main-wallet.ts b/sdk/oko_cosmos_kit/src/main-wallet.ts index 13b4153c0..8f5c95dad 100644 --- a/sdk/oko_cosmos_kit/src/main-wallet.ts +++ b/sdk/oko_cosmos_kit/src/main-wallet.ts @@ -46,9 +46,7 @@ export class OkoMainWallet extends MainWalletBase { throw new Error("Failed to initialize OkoCosmosWallet"); } - this.initClientDone( - new OkoWalletClient(cosmosWallet.data, options.loginProvider), - ); + this.initClientDone(new OkoWalletClient(cosmosWallet.data)); } catch (error) { this.initClientError(error as Error); } diff --git a/sdk/oko_cosmos_kit/src/types.ts b/sdk/oko_cosmos_kit/src/types.ts index 33438f9c4..ea84895d1 100644 --- a/sdk/oko_cosmos_kit/src/types.ts +++ b/sdk/oko_cosmos_kit/src/types.ts @@ -1,20 +1,8 @@ import type { Wallet } from "@cosmos-kit/core"; -import type { SignInType } from "@oko-wallet/oko-sdk-core"; - -export interface OkoLoginMethod { - provider: SignInType; -} export interface OkoWalletOptions { apiKey: string; sdkEndpoint?: string; - loginMethods?: OkoLoginMethod[]; -} - -export interface OkoWalletInternalOptions { - apiKey: string; - sdkEndpoint?: string; - loginProvider: SignInType; } -export type OkoWalletInfo = Wallet & { options: OkoWalletInternalOptions }; +export type OkoWalletInfo = Wallet & { options: OkoWalletOptions }; diff --git a/sdk/oko_interchain_kit/src/constant.ts b/sdk/oko_interchain_kit/src/constant.ts index 147d84f42..d13b4b01b 100644 --- a/sdk/oko_interchain_kit/src/constant.ts +++ b/sdk/oko_interchain_kit/src/constant.ts @@ -1,25 +1,2 @@ export const OKO_ICON = "data:image/png;base64,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"; - -export const PROVIDER_CONFIG: Record = { - google: { - logo: OKO_ICON, - name: "Oko Wallet (Google)", - }, - email: { - logo: OKO_ICON, - name: "Oko Wallet (Email)", - }, - x: { - logo: OKO_ICON, - name: "Oko Wallet (X)", - }, - telegram: { - logo: OKO_ICON, - name: "Oko Wallet (Telegram)", - }, - discord: { - logo: OKO_ICON, - name: "Oko Wallet (Discord)", - }, -}; diff --git a/sdk/oko_interchain_kit/src/index.ts b/sdk/oko_interchain_kit/src/index.ts index 54c93b1e3..bd8469d44 100644 --- a/sdk/oko_interchain_kit/src/index.ts +++ b/sdk/oko_interchain_kit/src/index.ts @@ -1,43 +1,18 @@ -import type { SignInType } from "@oko-wallet/oko-sdk-core"; - import { OkoWallet } from "./oko-wallet"; import { okoWalletInfo } from "./registry"; import type { OkoWalletOptions } from "./types"; import { initializeOkoCosmosWallet } from "./init"; -import { PROVIDER_CONFIG } from "./constant"; -export const makeOkoWallets = (options: OkoWalletOptions): OkoWallet[] => { +export const makeOkoWallet = (options: OkoWalletOptions): OkoWallet => { if (typeof window === "undefined") { throw new Error( "Oko Wallet can only be initialized in browser environment", ); } - // If no loginMethods specified, use all available providers - const providers = - options.loginMethods?.map((m) => m.provider) ?? - (Object.keys(PROVIDER_CONFIG) as SignInType[]); - - return providers.map((provider) => { - const providerConfig = PROVIDER_CONFIG[provider]; - - const internalOptions = { - ...options, - loginProvider: provider, - }; - - const okoClient = initializeOkoCosmosWallet(internalOptions); - - const walletInfo = { - ...okoWalletInfo, - name: okoWalletInfo.name + "_" + provider, - prettyName: providerConfig?.name || okoWalletInfo.prettyName, - logo: okoWalletInfo.logo, - }; - - return new OkoWallet(walletInfo, okoClient, provider); - }); + const okoClient = initializeOkoCosmosWallet(options); + return new OkoWallet(okoWalletInfo, okoClient); }; export { okoWalletInfo } from "./registry"; -export type { OkoLoginMethod, OkoWalletOptions } from "./types"; +export type { OkoWalletOptions } from "./types"; diff --git a/sdk/oko_interchain_kit/src/oko-wallet.ts b/sdk/oko_interchain_kit/src/oko-wallet.ts index 5271c24d5..7ec311ca1 100644 --- a/sdk/oko_interchain_kit/src/oko-wallet.ts +++ b/sdk/oko_interchain_kit/src/oko-wallet.ts @@ -1,5 +1,4 @@ import { CosmosWallet } from "@interchain-kit/core"; -import type { SignInType } from "@oko-wallet/oko-sdk-core"; import type { BroadcastMode, SignOptions, @@ -14,7 +13,6 @@ import type { OkoCosmosWalletInterface } from "@oko-wallet/oko-sdk-cosmos"; */ export class OkoWallet extends CosmosWallet { private okoClient: OkoCosmosWalletInterface; - private loginProvider: SignInType; defaultSignOptions: { preferNoSetFee: boolean; preferNoSetMemo: boolean; @@ -25,14 +23,9 @@ export class OkoWallet extends CosmosWallet { disableBalanceCheck: false, }; - constructor( - walletInfo: Wallet, - okoClient: OkoCosmosWalletInterface, - loginProvider: SignInType, - ) { + constructor(walletInfo: Wallet, okoClient: OkoCosmosWalletInterface) { super(walletInfo); this.okoClient = okoClient; - this.loginProvider = loginProvider; // Expose client for interchain-kit (this as any).client = okoClient; @@ -61,7 +54,7 @@ export class OkoWallet extends CosmosWallet { // If not signed in, trigger the sign-in flow if (!publicKey) { - await this.okoClient.okoWallet.signIn(this.loginProvider); + await this.okoClient.okoWallet.openSignInModal(); } const key = await this.okoClient.getKey(chainId); diff --git a/sdk/oko_interchain_kit/src/types.ts b/sdk/oko_interchain_kit/src/types.ts index 917a6960c..9e58c3753 100644 --- a/sdk/oko_interchain_kit/src/types.ts +++ b/sdk/oko_interchain_kit/src/types.ts @@ -1,20 +1,4 @@ -import type { Wallet } from "@interchain-kit/core"; -import type { SignInType } from "@oko-wallet/oko-sdk-core"; - -export interface OkoLoginMethod { - provider: SignInType; -} - export interface OkoWalletOptions { apiKey: string; sdkEndpoint?: string; - loginMethods?: OkoLoginMethod[]; -} - -export interface OkoWalletInternalOptions { - apiKey: string; - sdkEndpoint?: string; - loginProvider: SignInType; } - -export type OkoWalletInfo = Wallet & { options: OkoWalletInternalOptions }; diff --git a/sdk/oko_sdk_core/package.json b/sdk/oko_sdk_core/package.json index 4780595f6..1dbeb739d 100644 --- a/sdk/oko_sdk_core/package.json +++ b/sdk/oko_sdk_core/package.json @@ -34,7 +34,8 @@ "@keplr-wallet/types": "0.12.297", "@oko-wallet/bytes": "^0.0.3-alpha.65", "@oko-wallet/oko-types": "^0.0.1-alpha.5", - "@oko-wallet/stdlib-js": "^0.0.2-rc.45" + "@oko-wallet/stdlib-js": "^0.0.2-rc.45", + "preact": "^10.25.4" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.0", diff --git a/sdk/oko_sdk_core/src/methods/open_sign_in_modal.ts b/sdk/oko_sdk_core/src/methods/open_sign_in_modal.ts new file mode 100644 index 000000000..5245dc811 --- /dev/null +++ b/sdk/oko_sdk_core/src/methods/open_sign_in_modal.ts @@ -0,0 +1,27 @@ +import type { OkoWalletInterface } from "@oko-wallet-sdk-core/types"; +import { renderSignInModal } from "@oko-wallet-sdk-core/ui/signin_modal"; + +const state = { isModalOpen: false }; + +export async function openSignInModal(this: OkoWalletInterface): Promise { + await this.waitUntilInitialized; + + if (state.isModalOpen) { + return; + } + state.isModalOpen = true; + + return new Promise((resolve, reject) => { + renderSignInModal({ + onSelect: async (provider) => { + await this.signIn(provider); + state.isModalOpen = false; + resolve(); + }, + onClose: () => { + state.isModalOpen = false; + reject(new Error("Sign in cancelled")); + }, + }); + }); +} diff --git a/sdk/oko_sdk_core/src/oko.ts b/sdk/oko_sdk_core/src/oko.ts index 72ae547e9..93acd44a2 100644 --- a/sdk/oko_sdk_core/src/oko.ts +++ b/sdk/oko_sdk_core/src/oko.ts @@ -1,6 +1,7 @@ import pJson from "../package.json"; import { sendMsgToIframe } from "./methods/send_msg_to_iframe"; import { openModal } from "./methods/open_modal"; +import { openSignInModal } from "./methods/open_sign_in_modal"; import { signIn } from "./methods/sign_in"; import { signOut } from "./methods/sign_out"; import { getPublicKey } from "./methods/get_public_key"; @@ -21,6 +22,7 @@ OkoWallet.version = pJson.version; const ptype: OkoWalletInterface = OkoWallet.prototype; ptype.openModal = openModal; +ptype.openSignInModal = openSignInModal; ptype.closeModal = closeModal; ptype.sendMsgToIframe = sendMsgToIframe; ptype.signIn = signIn; diff --git a/sdk/oko_sdk_core/src/types/oko_wallet.ts b/sdk/oko_sdk_core/src/types/oko_wallet.ts index d8a0c7767..04a3e2f1b 100644 --- a/sdk/oko_sdk_core/src/types/oko_wallet.ts +++ b/sdk/oko_sdk_core/src/types/oko_wallet.ts @@ -35,6 +35,7 @@ export interface OkoWalletInterface { openModal: ( msg: OkoWalletMsgOpenModal, ) => Promise>; + openSignInModal: () => Promise; closeModal: () => void; sendMsgToIframe: (msg: OkoWalletMsg) => Promise; signIn: (type: SignInType) => Promise; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/components/default_view.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/components/default_view.tsx new file mode 100644 index 000000000..5b5052f4c --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/components/default_view.tsx @@ -0,0 +1,88 @@ +import { type FunctionComponent as FC } from "preact"; + +import type { SignInType } from "@oko-wallet-sdk-core/types/oauth"; +import type { ResolvedTheme } from "../types"; +import { S3_LOGO_URL, S3_LOGO_WITH_NAME_URL } from "../icons"; +import { ProviderButton } from "./provider_button"; +import { + EmailIcon, + GoogleIcon, + XSmallIcon, + TelegramSmallIcon, + AppleSmallIcon, + ChevronRightIcon, + ExternalLinkIcon, +} from "./icons"; + +export interface DefaultViewProps { + theme: ResolvedTheme; + onSelect: (provider: SignInType) => void; + onShowSocials: () => void; +} + +export const DefaultView: FC = ({ + theme, + onSelect, + onShowSocials, +}) => { + return ( +
+
+ Oko +
+
+ } + label="Email" + onClick={() => onSelect("email")} + /> + } + label="Google" + onClick={() => onSelect("google")} + /> + +
+ +
+ ); +}; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/components/icons.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/components/icons.tsx new file mode 100644 index 000000000..dd1f36eab --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/components/icons.tsx @@ -0,0 +1,207 @@ +import { type FunctionComponent as FC } from "preact"; + +export const GoogleIcon: FC = () => ( + + + + + + +); + +export const EmailIcon: FC = () => ( + + + + +); + +export const XIcon: FC = () => ( + + + +); + +export const XSmallIcon: FC = () => ( + + + +); + +export const TelegramIcon: FC = () => ( + + + + + + + + + + +); + +export const TelegramSmallIcon: FC = () => ( + + + + + + + + + + +); + +export const DiscordIcon: FC = () => ( + + + +); + +export const AppleIcon: FC = () => ( + + + +); + +export const AppleSmallIcon: FC = () => ( + + + +); + +export const CloseIcon: FC = () => ( + + + +); + +export const ChevronLeftIcon: FC = () => ( + + + +); + +export const ChevronRightIcon: FC = () => ( + + + +); + +export const ExternalLinkIcon: FC = () => ( + + + +); + +export const SpinnerLoadingIcon: FC = () => ( + + + +); + +export const SpinnerFailedIcon: FC = () => ( + + + +); diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/components/progress_view.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/components/progress_view.tsx new file mode 100644 index 000000000..6bd0e7ca4 --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/components/progress_view.tsx @@ -0,0 +1,58 @@ +import { type FunctionComponent as FC } from "preact"; + +import type { SignInType } from "@oko-wallet-sdk-core/types/oauth"; +import { + GoogleIcon, + EmailIcon, + XIcon, + TelegramIcon, + DiscordIcon, + SpinnerLoadingIcon, + SpinnerFailedIcon, +} from "./icons"; + +export interface ProgressViewProps { + status: "loading" | "failed"; + provider: SignInType; + onRetry: () => void; +} + +const PROVIDER_ICONS: Record = { + email: EmailIcon, + google: GoogleIcon, + x: XIcon, + telegram: TelegramIcon, + discord: DiscordIcon, +}; + +export const ProgressView: FC = ({ + status, + provider, + onRetry, +}) => { + const isLoading = status === "loading"; + const ProviderIcon = PROVIDER_ICONS[provider]; + + return ( +
+
+ + + + + {isLoading ? : } + +
+
+ {isLoading ? "Signing in" : "Login failed"} +
+ {status === "failed" && ( + + )} +
+ ); +}; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/components/provider_button.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/components/provider_button.tsx new file mode 100644 index 000000000..980a4528e --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/components/provider_button.tsx @@ -0,0 +1,30 @@ +import { type FunctionComponent, type ComponentChildren } from "preact"; + +export interface ProviderButtonProps { + icon: ComponentChildren; + label: string; + onClick?: () => void; + disabled?: boolean; + chevron?: ComponentChildren; +} + +export const ProviderButton: FunctionComponent = ({ + icon, + label, + onClick, + disabled = false, + chevron, +}) => { + return ( + + ); +}; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/components/socials_view.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/components/socials_view.tsx new file mode 100644 index 000000000..c0864c0cf --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/components/socials_view.tsx @@ -0,0 +1,47 @@ +import { type FunctionComponent as FC } from "preact"; + +import type { SignInType } from "@oko-wallet-sdk-core/types/oauth"; +import { ProviderButton } from "./provider_button"; +import { + XIcon, + TelegramIcon, + DiscordIcon, + AppleIcon, + ChevronLeftIcon, +} from "./icons"; + +export interface SocialsViewProps { + onSelect: (provider: SignInType) => void; + onBack: () => void; +} + +export const SocialsView: FC = ({ onSelect, onBack }) => { + return ( +
+
+ + Login or sign up +
+
+ } + label="X" + onClick={() => onSelect("x")} + /> + } + label="Telegram" + onClick={() => onSelect("telegram")} + /> + } + label="Discord" + onClick={() => onSelect("discord")} + /> + } label="Apple" disabled /> +
+
+ ); +}; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/hooks/use_theme.ts b/sdk/oko_sdk_core/src/ui/signin_modal/hooks/use_theme.ts new file mode 100644 index 000000000..131f71d3d --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/hooks/use_theme.ts @@ -0,0 +1,74 @@ +import { useState, useEffect } from "preact/hooks"; + +import type { SignInModalTheme, ResolvedTheme } from "../types"; + +function getSystemTheme(): ResolvedTheme { + if ( + typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + return "dark"; + } + return "light"; +} + +function getHostTheme(): ResolvedTheme | null { + if (typeof document === "undefined") { + return null; + } + + const root = document.documentElement; + const body = document.body; + + const dataTheme = root.dataset.theme || body?.dataset.theme; + if (dataTheme === "dark" || dataTheme === "light") { + return dataTheme; + } + + if (root.classList.contains("dark") || body?.classList.contains("dark")) { + return "dark"; + } + if (root.classList.contains("light") || body?.classList.contains("light")) { + return "light"; + } + + const colorScheme = getComputedStyle(root).colorScheme; + if (colorScheme === "dark" || colorScheme === "light") { + return colorScheme; + } + + return null; +} + +export function resolveTheme(theme: SignInModalTheme): ResolvedTheme { + if (theme === "light" || theme === "dark") { + return theme; + } + return getHostTheme() ?? getSystemTheme(); +} + +export function useTheme(theme: SignInModalTheme): ResolvedTheme { + const [resolvedTheme, setResolvedTheme] = useState(() => + resolveTheme(theme), + ); + + useEffect(() => { + if (theme !== "system") { + setResolvedTheme(theme); + return; + } + + const updateTheme = () => { + setResolvedTheme(getHostTheme() ?? getSystemTheme()); + }; + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + mediaQuery.addEventListener("change", updateTheme); + + return () => { + mediaQuery.removeEventListener("change", updateTheme); + }; + }, [theme]); + + return resolvedTheme; +} diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/icons.ts b/sdk/oko_sdk_core/src/ui/signin_modal/icons.ts new file mode 100644 index 000000000..527d92bda --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/icons.ts @@ -0,0 +1,12 @@ +const S3_BUCKET_URL = + "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/icons"; + +export const S3_LOGO_URL = { + light: `${S3_BUCKET_URL}/oko_logo_light.png`, + dark: `${S3_BUCKET_URL}/oko_logo_dark.png`, +}; + +export const S3_LOGO_WITH_NAME_URL = { + light: `${S3_BUCKET_URL}/oko_logo_with_name_light.svg`, + dark: `${S3_BUCKET_URL}/oko_logo_with_name_dark.svg`, +}; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/index.ts b/sdk/oko_sdk_core/src/ui/signin_modal/index.ts new file mode 100644 index 000000000..61a212fae --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/index.ts @@ -0,0 +1,2 @@ +export { renderSignInModal } from "./render"; +export type { SignInModalOptions, SignInModalTheme } from "./types"; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/render.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/render.tsx new file mode 100644 index 000000000..1bb024efd --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/render.tsx @@ -0,0 +1,54 @@ +import { render } from "preact"; + +import { SignInModal } from "./signin_modal"; +import { modalStyles } from "./styles"; +import { resolveTheme } from "./hooks/use_theme"; +import type { SignInModalOptions } from "./types"; + +const SIGNIN_MODAL_CONTAINER_ID = "oko-signin-modal-root"; + +export function renderSignInModal(options: SignInModalOptions) { + if (typeof document === "undefined") { + throw new Error("renderSignInModal cannot be called in SSR environment"); + } + + const { onSelect, onClose, theme = "system" } = options; + + const container = document.createElement("div"); + container.id = SIGNIN_MODAL_CONTAINER_ID; + container.dataset.theme = resolveTheme(theme); + + const shadow = container.attachShadow({ mode: "closed" }); + + const styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync(modalStyles); + shadow.adoptedStyleSheets = [styleSheet]; + + const preactRoot = document.createElement("div"); + shadow.appendChild(preactRoot); + + const originalOverflow = document.body.style.overflow; + document.body.appendChild(container); + document.body.style.overflow = "hidden"; + + const cleanup = () => { + document.body.style.overflow = originalOverflow; + render(null, preactRoot); + container.remove(); + }; + + const handleClose = () => { + cleanup(); + onClose?.(); + }; + + const handleSelect = async (provider: Parameters[0]) => { + await onSelect(provider); + cleanup(); + }; + + render( + , + preactRoot, + ); +} diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/signin_modal.tsx b/sdk/oko_sdk_core/src/ui/signin_modal/signin_modal.tsx new file mode 100644 index 000000000..23899a0cd --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/signin_modal.tsx @@ -0,0 +1,106 @@ +import { type FunctionComponent as FC, type JSX } from "preact"; +import { useState, useEffect, useMemo } from "preact/hooks"; + +import type { SignInType } from "@oko-wallet-sdk-core/types/oauth"; +import type { SignInModalTheme, ProgressState } from "./types"; +import { useTheme } from "./hooks/use_theme"; +import { DefaultView } from "./components/default_view"; +import { SocialsView } from "./components/socials_view"; +import { ProgressView } from "./components/progress_view"; +import { CloseIcon } from "./components/icons"; + +export interface SignInModalProps { + onSelect: (provider: SignInType) => Promise; + onClose: () => void; + theme?: SignInModalTheme; +} + +type ViewType = "default" | "socials"; + +export const SignInModal: FC = ({ + onSelect, + onClose, + theme, +}) => { + const [view, setView] = useState("default"); + const [progress, setProgress] = useState(null); + const resolvedTheme = useTheme(theme || "system"); + + const handleSelect = async (provider: SignInType) => { + setProgress({ status: "loading", provider }); + try { + await onSelect(provider); + } catch { + setProgress({ status: "failed", provider }); + } + }; + + const handleRetry = () => { + setProgress(null); + setView("default"); + }; + + const handleOverlayClick = (e: JSX.TargetedMouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [onClose]); + + const content = useMemo(() => { + if (progress) { + return ( + + ); + } + + if (view === "socials") { + return ( + setView("default")} + /> + ); + } + + return ( + setView("socials")} + /> + ); + }, [progress, resolvedTheme, handleSelect]); + + return ( +
+
+ +
{content}
+
+
+ ); +}; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/styles.ts b/sdk/oko_sdk_core/src/ui/signin_modal/styles.ts new file mode 100644 index 000000000..c14f3e071 --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/styles.ts @@ -0,0 +1,463 @@ +const css = String.raw; + +export const modalStyles = css` + :host { + /* Font */ + --oko-font-family: + Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Colors - Light theme (from oko_common_ui color_tokens) */ + --oko-white: #ffffff; + --oko-gray-50: #fafafa; + --oko-gray-100: #f5f5f5; + --oko-gray-300: #d5d7da; + --oko-gray-400: #a4a7ae; + --oko-gray-500: #717680; + --oko-gray-600: #535862; + --oko-gray-700: #414651; + --oko-gray-900: #181d27; + --oko-gray-950: #0a0d12; + + /* Semantic tokens */ + --oko-bg-primary: var(--oko-white); + --oko-bg-primary-hover: var(--oko-gray-50); + --oko-bg-secondary: var(--oko-gray-50); + --oko-bg-tertiary: var(--oko-gray-100); + --oko-bg-overlay: var(--oko-gray-950); + --oko-text-primary: var(--oko-gray-900); + --oko-text-secondary: var(--oko-gray-700); + --oko-text-brand-secondary: var(--oko-gray-900); + --oko-fg-tertiary: var(--oko-gray-600); + --oko-fg-quaternary: var(--oko-gray-400); + --oko-fg-disabled-subtle: var(--oko-gray-300); + --oko-fg-brand-primary: var(--oko-gray-900); + --oko-border-primary: var(--oko-gray-300); + --oko-border-error-subtle: #fda29b; + + /* Shadows */ + --oko-shadow-xs: 0 1px 2px 0 rgba(16, 24, 40, 0.05); + --oko-shadow-lg: + 0 12px 16px -4px rgba(16, 24, 40, 0.08), + 0 4px 6px -2px rgba(16, 24, 40, 0.03); + + /* Spacing */ + --oko-spacing-xs: 4px; + --oko-radius-md: 8px; + --oko-radius-lg: 16px; + } + + /* Dark theme */ + :host([data-theme="dark"]) { + /* Colors - Dark theme (from oko_common_ui color_tokens) */ + --oko-gray-50: #f7f7f7; + --oko-gray-300: #cecfd2; + --oko-gray-400: #94979c; + --oko-gray-500: #85888e; + --oko-gray-600: #61656c; + --oko-gray-700: #373a41; + --oko-gray-800: #22262f; + --oko-gray-900: #13161b; + --oko-gray-950: #0c0e12; + + /* Semantic tokens - Dark */ + --oko-bg-primary: var(--oko-gray-950); + --oko-bg-primary-hover: var(--oko-gray-800); + --oko-bg-secondary: var(--oko-gray-900); + --oko-bg-tertiary: var(--oko-gray-800); + --oko-text-primary: var(--oko-gray-50); + --oko-text-secondary: var(--oko-gray-300); + --oko-text-brand-secondary: var(--oko-gray-300); + --oko-fg-tertiary: var(--oko-gray-400); + --oko-fg-quaternary: var(--oko-gray-600); + --oko-fg-disabled-subtle: var(--oko-gray-600); + --oko-fg-brand-primary: var(--oko-gray-50); + --oko-border-primary: var(--oko-gray-700); + --oko-border-error-subtle: #f04438; + + /* Shadows - Dark */ + --oko-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.2); + --oko-shadow-lg: + 0 12px 16px -4px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); + } + + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + .oko-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 2147483647; + isolation: isolate; + font-family: var(--oko-font-family); + } + + .oko-modal-container { + position: relative; + background: var(--oko-bg-primary); + border-radius: var(--oko-radius-lg); + box-shadow: var(--oko-shadow-lg); + width: 100%; + max-width: 400px; + margin: 16px; + overflow: hidden; + animation: oko-modal-fade-in 0.2s ease-out; + } + + @keyframes oko-modal-fade-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } + } + + .oko-modal-close { + position: absolute; + right: 24px; + top: 24px; + width: 36px; + height: 36px; + padding: var(--oko-radius-md); + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--oko-radius-md); + color: var(--oko-fg-tertiary); + transition: background-color 0.2s ease; + z-index: 1; + } + + .oko-modal-close:hover { + background: var(--oko-bg-secondary); + } + + .oko-modal-close svg { + width: 20px; + height: 20px; + } + + /* Progress View (Loading/Failed) */ + .oko-progress-view { + display: flex; + padding: 32px 24px 24px 24px; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 240px; + } + + .oko-progress-circle { + display: flex; + align-items: center; + justify-content: center; + width: 54px; + height: 54px; + position: relative; + } + + .oko-progress-circle .oko-provider-icon { + width: 48px; + height: 48px; + } + + .oko-progress-circle .oko-provider-icon svg { + width: 100%; + height: 100%; + } + + .oko-spinner-overlay { + position: absolute; + top: 50%; + left: 50%; + margin-top: -31px; + margin-left: -31px; + z-index: 10; + width: 62px; + height: 62px; + color: var(--oko-fg-brand-primary); + } + + .oko-spinner-overlay svg { + width: 100%; + height: 100%; + } + + .oko-spinner-overlay.oko-spinning svg { + animation: oko-spin 1s linear infinite; + } + + .oko-spinner-overlay:not(.oko-spinning) { + color: var(--oko-border-error-subtle); + } + + @keyframes oko-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + .oko-progress-text { + margin-top: 9px; + font-size: 16px; + font-weight: 500; + line-height: 24px; + color: var(--oko-text-primary); + text-align: center; + } + + .oko-retry-btn { + display: flex; + height: 20px; + justify-content: center; + align-items: center; + gap: 4px; + align-self: stretch; + cursor: pointer; + margin-top: 16px; + background: none; + border: none; + font-family: var(--oko-font-family); + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: var(--oko-text-brand-secondary); + } + + .oko-retry-btn:hover { + opacity: 0.8; + } + + /* Default View */ + .oko-default-view { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 20px 20px 20px; + } + + .oko-logo-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; + margin-bottom: 36px; + } + + .oko-logo-wrapper img { + display: block; + } + + /* Socials View */ + .oko-socials-view { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; + } + + .oko-back-row { + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; + margin-bottom: 26px; + } + + .oko-back-btn { + width: 24px; + height: 24px; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--oko-text-primary); + padding: 0; + } + + .oko-back-btn svg { + width: 24px; + height: 24px; + } + + .oko-back-title { + flex: 1; + text-align: center; + font-size: 14px; + font-weight: 500; + color: var(--oko-text-primary); + padding-right: 24px; /* Balance the back button */ + } + + /* Provider List */ + .oko-provider-list { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + } + + .oko-socials-list { + gap: 12px; + } + + /* Provider Button */ + .oko-provider-btn { + display: flex; + justify-content: center; + align-items: center; + gap: var(--oko-spacing-xs); + width: 100%; + height: 40px; + padding: 10px 14px; + border: 1px solid var(--oko-border-primary); + border-radius: var(--oko-radius-md); + background: var(--oko-bg-primary); + cursor: pointer; + font-family: var(--oko-font-family); + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: var(--oko-text-primary); + box-shadow: var(--oko-shadow-xs); + transition: background 0.2s ease; + outline: none; + } + + .oko-provider-btn:hover:not(:disabled) { + background: var(--oko-bg-primary-hover); + } + + .oko-provider-btn:focus { + box-shadow: + 0 0 0 4px rgba(152, 162, 179, 0.14), + 0 1px 2px 0 rgba(16, 24, 40, 0.05); + } + + .oko-provider-btn:disabled { + color: var(--oko-fg-quaternary); + border-color: var(--oko-fg-disabled-subtle); + cursor: not-allowed; + } + + .oko-provider-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .oko-provider-icon svg { + width: 100%; + height: 100%; + } + + .oko-provider-label { + padding: 0 2px; + } + + /* Social Icons Wrapper (for Other Socials button) */ + .oko-social-icons-wrapper { + display: flex; + align-items: center; + position: relative; + } + + .oko-social-icon { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + } + + .oko-social-icon + .oko-social-icon { + margin-left: -2px; + } + + .oko-social-icon svg { + width: 100%; + height: 100%; + } + + .oko-chevron-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + color: var(--oko-fg-quaternary); + } + + .oko-chevron-icon svg { + width: 100%; + height: 100%; + } + + /* Footer */ + .oko-modal-footer { + margin-top: 28px; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + + .oko-footer-logo { + display: block; + width: 52px; + height: 20px; + } + + .oko-footer-link { + font-size: 12px; + font-weight: 500; + color: var(--oko-text-secondary); + text-decoration: underline; + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + } + + .oko-footer-link:hover { + color: var(--oko-text-primary); + } + + .oko-external-icon { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + color: var(--oko-gray-500); + } + + .oko-external-icon svg { + width: 100%; + height: 100%; + } +`; diff --git a/sdk/oko_sdk_core/src/ui/signin_modal/types.ts b/sdk/oko_sdk_core/src/ui/signin_modal/types.ts new file mode 100644 index 000000000..25106aa4d --- /dev/null +++ b/sdk/oko_sdk_core/src/ui/signin_modal/types.ts @@ -0,0 +1,15 @@ +import type { SignInType } from "@oko-wallet-sdk-core/types/oauth"; + +export type SignInModalTheme = "light" | "dark" | "system"; +export type ResolvedTheme = "light" | "dark"; + +export interface SignInModalOptions { + onSelect: (provider: SignInType) => Promise; + onClose?: () => void; + theme?: SignInModalTheme; +} + +export interface ProgressState { + status: "loading" | "failed"; + provider: SignInType; +} diff --git a/sdk/oko_sdk_core/tsconfig.json b/sdk/oko_sdk_core/tsconfig.json index b015ac2b5..05186149e 100644 --- a/sdk/oko_sdk_core/tsconfig.json +++ b/sdk/oko_sdk_core/tsconfig.json @@ -3,6 +3,8 @@ "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", + "jsx": "react-jsx", + "jsxImportSource": "preact", "lib": ["dom", "ES2020", "ESNext.AsyncIterable"], "skipLibCheck": true, "sourceMap": true, diff --git a/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts b/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts index 98ac11590..158d1cfdc 100644 --- a/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts +++ b/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts @@ -65,7 +65,7 @@ export class OkoStandardWallet implements Wallet { if (!existingKey) { // Trigger OAuth sign-in - await this.#wallet.okoWallet.signIn("google"); + await this.#wallet.okoWallet.openSignInModal(); // Re-check after sign-in existingKey = await this.#wallet.okoWallet.getPublicKeyEd25519(); diff --git a/ui/oko_common_ui/src/checkbox/checkbox.module.scss b/ui/oko_common_ui/src/checkbox/checkbox.module.scss index 9dd1f2903..7e3d8c821 100644 --- a/ui/oko_common_ui/src/checkbox/checkbox.module.scss +++ b/ui/oko_common_ui/src/checkbox/checkbox.module.scss @@ -16,17 +16,14 @@ align-items: center; } -[data-theme="light"] { - .checkboxInput { - color: var(--white); - } -} [data-theme="dark"] { .checkboxInput { color: var(--gray-700); } } + .checkboxInput { + color: var(--white); display: flex; justify-content: center; diff --git a/yarn.lock b/yarn.lock index a97c802d3..123ab05c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10916,6 +10916,7 @@ __metadata: chalk: "npm:^5.5.0" del: "npm:^8.0.1" jest: "npm:^30.1.3" + preact: "npm:^10.25.4" rollup: "npm:^4.0.0" rollup-plugin-dts: "npm:^6.2.1" rollup-plugin-tsconfig-paths: "npm:^1.5.2" @@ -11281,7 +11282,7 @@ __metadata: languageName: unknown linkType: soft -"@oko-wallet/teddsa-api-lib@workspace:crypto/teddsa/api_lib": +"@oko-wallet/teddsa-api-lib@workspace:*, @oko-wallet/teddsa-api-lib@workspace:crypto/teddsa/api_lib": version: 0.0.0-use.local resolution: "@oko-wallet/teddsa-api-lib@workspace:crypto/teddsa/api_lib" dependencies: @@ -11310,6 +11311,7 @@ __metadata: "@oko-wallet/bytes": "npm:^0.0.3-alpha.65" "@oko-wallet/frost-ed25519-keplr-wasm": "workspace:*" "@oko-wallet/stdlib-js": "npm:^0.0.2-rc.42" + "@oko-wallet/teddsa-api-lib": "workspace:*" "@oko-wallet/teddsa-interface": "workspace:*" "@oko-wallet/teddsa-wasm-mock": "workspace:*" "@types/node": "npm:^24.10.1" @@ -35860,6 +35862,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.25.4": + version: 10.28.2 + resolution: "preact@npm:10.28.2" + checksum: 10c0/eb60bf526eb6971701e6ac9c25236aca451f17f99e9c24704419196989b15bb576ed3101e084b151cd0fb30546b3e5e1ba73b774e8be2f2ed8187db42ec65faf + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1"