-
Notifications
You must be signed in to change notification settings - Fork 80
Add PII-safe EIN hash lookup for importers #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
|
Comment on lines
+25
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the PII-safe lookup path, storing a plain SHA-256 of an EIN is reversible by offline enumeration if a database snapshot or read-only access leaks, because EINs have a small, fixed format. This same digest is persisted for new registrations and backfilled in the migration, so once plaintext EINs are encrypted or removed the lookup column still exposes the identifier; use a secret-keyed HMAC/pepper for deterministic equality instead. Useful? React with 👍 / 👎. |
||
|
|
||
| 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]!; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On any existing deployment that already has two importer rows with the same non-null
ein(which the previous schema allowed, sinceimporters.einwas not unique and registration only checkeduser_id), the backfill will assign the sameein_hashto both rows and this unique index creation will abortmigrate(), preventing the service from starting. If global EIN uniqueness is required, the migration needs to detect/dedupe legacy conflicts first or defer the unique constraint until data has been cleaned.Useful? React with 👍 / 👎.