Move staff PINs out of the shipped binary into hashed database rows
Three StaffUser constants carried plaintext PINs (1234/2345/3456) in auth_controller.dart. Every build shipped every till's credentials, readable by anyone who unzipped the APK. Across 100 deployed devices that is one credential, not a hundred. - Schema v5 adds a staff table. Only a PBKDF2-HMAC-SHA256 hash and a per-user random salt are stored; the PIN itself exists nowhere, including there. 12,000 iterations, tuned so one sign-in is imperceptible while working through all 10,000 four-digit PINs against a stolen database takes ~15 minutes per account instead of milliseconds. - Verification is constant-time. String == returns at the first differing byte, and that timing leaks how much of a guess was right. - StaffUser no longer has a pin field at all, so the credential cannot drift back into memory, into widgets, or into a const declaration. - Weak PINs are refused: under four digits, non-numeric, repeated digits, and sequences. Two staff cannot share a PIN — the till identifies a cashier by PIN alone, so a shared one would attribute bills to whichever row was checked first. - The last admin cannot be demoted or deactivated. A till with no admin cannot be administered, including to appoint one, and recovering means editing the database by hand. - Staff are deactivated, never deleted, so bills already rung keep naming a real person. Seed accounts are now 4821/5093/6274 rather than 1234/2345/3456 — the weak-PIN rule refuses the old ones, and a default the rule itself would reject is not a defensible default. All three are flagged must-change-pin so they get a shop trading on day one without becoming permanent. Store details are now editable data, not compile-time constants. Name, address, GSTIN and phone persist to the database and are read back rather than falling through to the build's constants, which would silently undo a failed save. GSTIN is format-validated including the state code — it prints on every invoice as a legal requirement, so a typo is a compliance problem across hundreds of bills before anyone notices. Tests: 141 -> 160. Includes a test that reads every column of every staff row and asserts no seed PIN appears anywhere in the database. Migration test now asserts v5 and that an upgraded terminal comes up with the staff table present but empty — seeding is the store's job on first open, so an existing shop is never handed accounts it did not create. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ class AppDatabase {
|
||||
static final AppDatabase instance = AppDatabase._();
|
||||
|
||||
static const String _fileName = 'nearle_pos.db';
|
||||
static const int _version = 4;
|
||||
static const int _version = 5;
|
||||
|
||||
Database? _db;
|
||||
|
||||
@@ -70,6 +70,7 @@ class AppDatabase {
|
||||
);
|
||||
}
|
||||
if (from < 4) await _upgradeToV4(db, from: from);
|
||||
if (from < 5) await db.execute(_createStaff);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -129,6 +130,7 @@ class AppDatabase {
|
||||
Tables.customers,
|
||||
Tables.parkedBills,
|
||||
Tables.syncLog,
|
||||
Tables.staff,
|
||||
Tables.meta,
|
||||
]) {
|
||||
batch.delete(t);
|
||||
@@ -265,6 +267,7 @@ class AppDatabase {
|
||||
|
||||
// ------------------------------------------------------------- archive
|
||||
await db.execute(_createDayArchive);
|
||||
await db.execute(_createStaff);
|
||||
|
||||
// ----------------------------------------------------------------- meta
|
||||
await db.execute('''
|
||||
@@ -327,6 +330,27 @@ const String _createSyncLog = '''
|
||||
/// here first — otherwise "Bills Today" would collapse to zero the moment a
|
||||
/// mid-shift sync ran. Keyed by cashier as well as date, because once the
|
||||
/// orders are gone this row is the only thing left to settle a till against.
|
||||
/// Staff who can sign in at this terminal.
|
||||
///
|
||||
/// Replaces three `StaffUser` literals with plaintext PINs that shipped inside
|
||||
/// every build. Only the PBKDF2 hash and its salt are stored — the PIN itself
|
||||
/// exists nowhere, including here.
|
||||
const String _createStaff = '''
|
||||
CREATE TABLE staff (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
pin_hash TEXT NOT NULL,
|
||||
pin_salt TEXT NOT NULL,
|
||||
-- Set on a seeded or reset account, cleared once the person picks their
|
||||
-- own, so a shop running a default PIN is at least visibly nagged.
|
||||
must_change_pin INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
''';
|
||||
|
||||
const String _createDayArchive = '''
|
||||
CREATE TABLE day_archive (
|
||||
business_date TEXT NOT NULL,
|
||||
@@ -358,6 +382,7 @@ class Tables {
|
||||
static const String orderItems = 'order_items';
|
||||
static const String parkedBills = 'parked_bills';
|
||||
static const String syncLog = 'sync_log';
|
||||
static const String staff = 'staff';
|
||||
static const String meta = 'app_meta';
|
||||
}
|
||||
|
||||
@@ -381,6 +406,14 @@ class MetaKeys {
|
||||
|
||||
static const String storeId = 'store_id';
|
||||
|
||||
/// Printed on every invoice, so they are a legal requirement rather than
|
||||
/// decoration — and must be editable without a rebuild.
|
||||
static const String storeName = 'store_name';
|
||||
static const String storeAddress = 'store_address';
|
||||
static const String storeGstin = 'store_gstin';
|
||||
static const String storePhone = 'store_phone';
|
||||
static const String storePlan = 'store_plan';
|
||||
|
||||
/// Printer chosen in Settings. Stored as the printer's `url`, which is what
|
||||
/// `Printing.directPrintPdf` needs to target it without a dialog.
|
||||
static const String printerUrl = 'printer_url';
|
||||
|
||||
Reference in New Issue
Block a user