Skip to content

Sands-45/pin-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PinAuth (WIP)

Overview

PinAuth 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 pin-auth

then

import { PinAuth } from "pin-auth";

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 for encryptPin)
  • 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 to pin-auth-store)

Note: deviceKey (via deviceKeyString or deviceKeyRaw) must be provided at instantiation time if you intend to use methods that store or retrieve encrypted data from IndexedDB (addPinAuthData, updatePinAuthData, getDecryptedPinAuthDataById, getAllDecryptedPinAuthData, and verifyPin when 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 orgId and the instance's salt).
  • Returns an object with an auth property 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 deviceKey to be set on the PinAuth instance.
  • Each object in the data array must be an AuthDataType, which includes an auth object (typically generated by encryptPin).

updatePinAuthData(data: AuthDataType[]): Promise<void>

  • Encrypts (using deviceKey) and updates existing user data in local IndexedDB.
  • Behaves like addPinAuthData (uses bulkPut which adds or overwrites).
  • Requires a deviceKey.

getDecryptedPinAuthDataById(id: string): Promise<AuthDataType | null>

  • Retrieves a specific user's data record by id from IndexedDB.
  • Decrypts the data using the deviceKey.
  • Returns the decrypted AuthDataType object (which includes the auth object) or null if 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 AuthDataType objects.
  • 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 pin against all locally stored and encrypted user data.
  • Internally, this method calls getAllDecryptedPinAuthData() to fetch and decrypt all records using the deviceKey.
  • It then iterates through each decrypted record, deriving an HMAC key using this.orgId and the record's auth.s (salt).
  • It computes the HMAC of the input pin and compares it to the record's auth.h (hash).
  • If a match is found with any record, it returns the user data part of that record (excluding the auth property itself). Otherwise, returns null.
  • This method requires the deviceKey to be initialized on the PinAuth instance to decrypt the stored data.

PIN Uniqueness Check (Server-Side or Admin Flow)

isPinUnique(pin: string, existingAuthObjects: AuthObject[]): Promise<boolean>

  • Checks if a given pin is unique among an array of existingAuthObjects.
  • Each object in existingAuthObjects must be an AuthObject (containing h and s).
  • For each AuthObject, this method derives an HMAC key using this.orgId and AuthObject.s, then signs the input pin and compares it to AuthObject.h.
  • Returns false if the pin matches any of the AuthObjects in the array (i.e., the PIN is already in use).
  • Returns true if 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 of AuthObjects (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 device

On 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). The orgId acts as the password, and the salt (either instance-wide for encryptPin or user-specific from auth.s for verifyPin/isPinUnique) is used in this derivation.
  • The instance salt (used by encryptPin and stored in auth.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 as deviceKeyString. 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 auth object (h and s) is crucial for PIN verification. h is the HMAC of the PIN, and s is the salt used in generating that HMAC.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published