@use-stall/pin
v0.0.3
Published
Local Pin Authentication Ideally for POS Systems to be used with other authentication methods
Readme
Pin (WIP)
Overview
Pin is a lightweight, local-first cryptographic authentication helper designed for environments where offline login via PIN is required like POS system It supports:
- HMAC-based PIN hashing and verification
- Envelope encryption/decryption using a device key for data at rest in IndexedDB
- Secure client-side storage using IndexedDB (via Dexie.js)
- Configurable setup with organization ID, salt, and storage name
This package works both in the browser and on the server (with Web Crypto API support). The deviceKey is used for encrypting and decrypting data stored locally in the browser's IndexedDB, which is necessary for the verifyPin method when operating on the client-side with stored data. The encryptPin and isPinUnique methods do not require a deviceKey.
Use this with other authentication methods
Installation
install using npm, bun or any package manager:
bun i @use-stall/pinthen
import { PinAuth } from "@use-stall/pin";Interfaces
export interface PinAuthConfig {
orgId: string;
salt?: Uint8Array;
deviceKeyRaw?: Uint8Array;
deviceKeyString?: string;
localDbName?: string;
}
export interface AuthObject {
h: string; // HMAC hash
s: string; // Salt used for HMAC
}
export interface AuthDataType {
auth: AuthObject;
[key: string]: any;
}Constructor
new PinAuth(config: PinAuthConfig)orgId– Unique string per organization (used as PBKDF2 password input)salt– Optional salt (random 16-byte array will be generated if not provided forencryptPin)deviceKeyRaw– Optional AES key as a raw Uint8Array (takes precedence over string) for client-side data encryption.deviceKeyString– Optional device key string (e.g., a Firestore document ID) for client-side data encryption. If provided, it will be automatically set and used.localDbName– Optional name for IndexedDB database (defaults topin-auth-store)
Note:
deviceKey(viadeviceKeyStringordeviceKeyRaw) must be provided at instantiation time if you intend to use methods that store or retrieve encrypted data from IndexedDB (addPinAuthData,updatePinAuthData,getDecryptedPinAuthDataById,getAllDecryptedPinAuthData, andverifyPinwhen used on client-side with local storage).
Methods
PIN Management (Server-Side or Admin Flow for generating AuthObject)
encryptPin(pin: string): Promise<{ auth: { h: string, s: string } }>
- Hashes a plain PIN using HMAC (derived from
orgIdand the instance'ssalt). - Returns an object with an
authproperty containing:h: base64 HMAC hash.s: base64-encoded salt (the instance's salt used for this encryption).
- This method does not use the
deviceKey.
Local Data Storage and Retrieval (Client-Side, uses deviceKey)
addPinAuthData(data: AuthDataType[]): Promise<void>
- Encrypts (using
deviceKey) and stores an array of user data to local IndexedDB. - Requires a
deviceKeyto be set on thePinAuthinstance. - Each object in the
dataarray must be anAuthDataType, which includes anauthobject (typically generated byencryptPin).
updatePinAuthData(data: AuthDataType[]): Promise<void>
- Encrypts (using
deviceKey) and updates existing user data in local IndexedDB. - Behaves like
addPinAuthData(usesbulkPutwhich adds or overwrites). - Requires a
deviceKey.
getDecryptedPinAuthDataById(id: string): Promise<AuthDataType | null>
- Retrieves a specific user's data record by
idfrom IndexedDB. - Decrypts the data using the
deviceKey. - Returns the decrypted
AuthDataTypeobject (which includes theauthobject) ornullif not found or if decryption fails. - Requires a
deviceKey.
getAllDecryptedPinAuthData(): Promise<AuthDataType[]>
- Retrieves all user data records from IndexedDB.
- Decrypts each record using the
deviceKey. - Returns an array of decrypted
AuthDataTypeobjects. - Requires a
deviceKey.
clearPinAuthData(): Promise<void>
- Clears all stored PIN auth data from IndexedDB.
PIN Verification (Client-Side with local storage)
verifyPin(pin: string): Promise<AuthDataType | null>
- Verifies a plain
pinagainst all locally stored and encrypted user data. - Internally, this method calls
getAllDecryptedPinAuthData()to fetch and decrypt all records using thedeviceKey. - It then iterates through each decrypted record, deriving an HMAC key using
this.orgIdand the record'sauth.s(salt). - It computes the HMAC of the input
pinand compares it to the record'sauth.h(hash). - If a match is found with any record, it returns the user data part of that record (excluding the
authproperty itself). Otherwise, returnsnull. - This method requires the
deviceKeyto be initialized on thePinAuthinstance to decrypt the stored data.
PIN Uniqueness Check (Server-Side or Admin Flow)
isPinUnique(pin: string, existingAuthObjects: AuthObject[]): Promise<boolean>
- Checks if a given
pinis unique among an array ofexistingAuthObjects. - Each object in
existingAuthObjectsmust be anAuthObject(containinghands). - For each
AuthObject, this method derives an HMAC key usingthis.orgIdandAuthObject.s, then signs the inputpinand compares it toAuthObject.h. - Returns
falseif thepinmatches any of theAuthObjects in the array (i.e., the PIN is already in use). - Returns
trueif no match is found (i.e., the PIN is unique relative to the provided list). - This method does not interact with IndexedDB or use the
deviceKey. It's intended for contexts where you have a collection ofAuthObjects (e.g., from a central database) and want to check if a new PIN conflicts.
Example Usage
On the Server (Admin Setup - Generating Auth Objects)
const authServer = new PinAuth({ orgId: "org_123" }); // No deviceKey needed here
const { auth: authObjectForUser1 } = await authServer.encryptPin("123456");
// User data to be sent to client (e.g., via API)
const userDataForClient = {
id: "user1",
name: "Anna",
// ... other user details
auth: authObjectForUser1, // Contains h and s
};
// Send userDataForClient to the client deviceOn the Client (POS Device Setup - Storing Encrypted Data)
const deviceKey = "abc123firestoreDocId"; // Unique key for this device
const authClient = new PinAuth({
orgId: "org_123",
deviceKeyString: deviceKey,
});
// Assume userDataFromServer is received from the server
// const userDataFromServer = { id: 'user1', name: 'Anna', auth: { h: '...', s: '...' } };
// const anotherUserDataFromServer = { id: 'user2', name: 'Ben', auth: { h: '...', s: '...' } };
// Store user data locally (encrypted with deviceKey)
await authClient.addPinAuthData([
userDataFromServer,
anotherUserDataFromServer,
]);Verifying PIN Locally (Client-Side)
// The user enters their PIN, e.g., '123456'
const pinAttempt = "123456";
// authClient is an instance of PinAuth with orgId and deviceKey configured
const matchedUser = await authClient.verifyPin(pinAttempt);
if (matchedUser) {
console.log("Logged in as:", matchedUser.name);
// matchedUser contains { id: 'user1', name: 'Anna', ... } (without the 'auth' property)
} else {
console.log("Invalid PIN or user not found.");
}Checking PIN Uniqueness (Server-Side/Admin)
// Assume authServer is an instance of PinAuth configured with the correct orgId
// (deviceKey is not needed for this operation)
const authServer = new PinAuth({ orgId: "org_123" });
// Assume `allUserAuthObjects` is an array of AuthObject items fetched from your central user database
// e.g., allUserAuthObjects = [ { h: "hash1", s: "salt1" }, { h: "hash2", s: "salt2" }, ... ];
const allUserAuthObjectsFromDb = [
/* ... load AuthObjects from your database ... */
];
const newPinCandidate = "newSecurePin123";
const isUnique = await authServer.isPinUnique(
newPinCandidate,
allUserAuthObjectsFromDb
);
if (isUnique) {
console.log(`PIN "${newPinCandidate}" is unique and can be assigned.`);
// Proceed to encrypt this new PIN and save it for the user
const { auth: newAuthObject } = await authServer.encryptPin(newPinCandidate);
// ... save newAuthObject for the user in your central database ...
} else {
console.log(
`PIN "${newPinCandidate}" is already in use. Please choose another.`
);
}Updating Local Data (Client-Side)
// Assume updatedUserDataArray contains AuthDataType objects with potentially new non-auth fields
// or new auth objects if PINs were changed server-side.
// const updatedUserDataArray = [ { id: 'user1', name: 'Anna Smith', auth: {h:'...', s:'...'} } ];
await authClient.updatePinAuthData(updatedUserDataArray);Clearing Local Data (Client-Side)
await authClient.clearPinAuthData();Security Notes
- PBKDF2 with HMAC-SHA256 is used to derive the HMAC key for PIN hashing (
encryptPin) and verification (verifyPin,isPinUnique). TheorgIdacts as the password, and thesalt(either instance-wide forencryptPinor user-specific fromauth.sforverifyPin/isPinUnique) is used in this derivation. - The instance
salt(used byencryptPinand stored inauth.s) should ideally be unique per organization or deployment if generated randomly. If a fixed salt is provided in config, ensure it's cryptographically strong. deviceKey(if used for client-side storage) is for envelope encryption of user data at rest using AES-GCM. Strings are UTF-8 encoded and padded to 32 bytes if used asdeviceKeyString. A new IV is used for every encryption operation.- IndexedDB is used for client-side storage. Ensure the environment where this runs is secure.
- The
authobject (hands) is crucial for PIN verification.his the HMAC of the PIN, andsis the salt used in generating that HMAC.
License
MIT
