diff --git a/apps/api/src/db.ts b/apps/api/src/db.ts index b743f01..8778f57 100644 --- a/apps/api/src/db.ts +++ b/apps/api/src/db.ts @@ -109,6 +109,7 @@ export async function migrate(): Promise { user_id UUID NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, legal_name TEXT NOT NULL, ein TEXT, + ein_hash TEXT, bond_id BIGINT UNIQUE NOT NULL, stellar_address TEXT NOT NULL, stellar_secret_encrypted TEXT, @@ -318,6 +319,15 @@ export async function migrate(): Promise { VALUES (1, 'initial key version') ON CONFLICT (key_version) DO NOTHING; + -- EIN plaintext is scheduled for AES-GCM encryption; ein_hash supports PII-safe equality lookup. + CREATE EXTENSION IF NOT EXISTS pgcrypto; + ALTER TABLE importers ADD COLUMN IF NOT EXISTS ein_hash TEXT; + UPDATE importers + SET ein_hash = encode(sha256(ein::bytea), 'hex') + WHERE ein IS NOT NULL AND ein_hash IS NULL; + CREATE UNIQUE INDEX IF NOT EXISTS idx_importers_ein_hash + ON importers(ein_hash) WHERE ein_hash IS NOT NULL; + -- EIN is now stored as AES-256-GCM JSON; migrate existing plain text at app layer ALTER TABLE importers ADD COLUMN IF NOT EXISTS ein_encrypted TEXT; ALTER TABLE importers ADD COLUMN IF NOT EXISTS ein_key_version INTEGER REFERENCES field_encryption_key_versions(key_version); diff --git a/apps/api/src/routes/importers.ts b/apps/api/src/routes/importers.ts index 4aab0d2..155c41c 100644 --- a/apps/api/src/routes/importers.ts +++ b/apps/api/src/routes/importers.ts @@ -1,4 +1,5 @@ import { Router, type Request, type Response } from "express"; +import { createHash } from "node:crypto"; import { Keypair } from "@stellar/stellar-sdk"; import { z } from "zod"; import { pool } from "../db.js"; @@ -21,6 +22,10 @@ const CreateImporterSchema = z.object({ initialRequiredCollateral: z.string().regex(/^\d+$/), }); +function hashEin(ein?: string): string | null { + return ein ? createHash("sha256").update(ein).digest("hex") : null; +} + importersRouter.post("/", async (req: Request, res: Response) => { const user = (req as AuthedRequest).user; if (user.role !== "importer") { @@ -34,6 +39,7 @@ importersRouter.post("/", async (req: Request, res: Response) => { return; } const { legalName, ein, bondId, initialRequiredCollateral } = parse.data; + const einHash = hashEin(ein); const ofacClear = await screenImporterEntity(legalName, ein); if (!ofacClear) { @@ -71,10 +77,10 @@ importersRouter.post("/", async (req: Request, res: Response) => { } const inserted = await pool.query( - `INSERT INTO importers (user_id, legal_name, ein, bond_id, stellar_address, stellar_secret_encrypted) - VALUES ($1, $2, $3, $4, $5, $6) + `INSERT INTO importers (user_id, legal_name, ein, ein_hash, bond_id, stellar_address, stellar_secret_encrypted) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, legal_name, ein, bond_id, stellar_address, created_at`, - [user.id, legalName, ein ?? null, bondId, kp.publicKey(), kp.secret()], + [user.id, legalName, ein ?? null, einHash, bondId, kp.publicKey(), kp.secret()], ); const importer = inserted.rows[0]!; diff --git a/scripts/admin.ts b/scripts/admin.ts index 63683c4..aaa501d 100644 --- a/scripts/admin.ts +++ b/scripts/admin.ts @@ -1,4 +1,5 @@ import { Command } from "commander"; +import { createHash } from "node:crypto"; import { pool } from "../apps/api/src/db.js"; import { contractClient, platformKeypair } from "../apps/api/src/stellar.js"; import { hashPassword } from "../apps/api/src/auth.js"; @@ -32,12 +33,15 @@ program const kp = Keypair.random(); const bondId = Math.floor(Math.random() * 1000000); const initialRequired = 0n; + const einHash = options.ein + ? createHash("sha256").update(options.ein).digest("hex") + : null; const inserted = await pool.query( - `INSERT INTO importers (user_id, legal_name, ein, bond_id, stellar_address, stellar_secret_encrypted) - VALUES ($1, $2, $3, $4, $5, $6) + `INSERT INTO importers (user_id, legal_name, ein, ein_hash, bond_id, stellar_address, stellar_secret_encrypted) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, - [userId, options.company, options.ein ?? null, bondId, kp.publicKey(), kp.secret()] + [userId, options.company, options.ein ?? null, einHash, bondId, kp.publicKey(), kp.secret()] ); const importerId = inserted.rows[0].id;