# Offline Login System with JWT, Password Hashes, and TOTP MFA (Browser)

This document contains a complete reference implementation for secure offline login in a browser environment.  

It supports:
- Per-user encrypted storage of JWTs, password hashes, and optional TOTP secrets.  
- Local password verification using bcrypt/Argon2id.  
- Optional offline MFA verification using Google Authenticator (TOTP).  
- Encrypted local storage with per-user device keys.  

---

## 1. IndexedDB Schema

```js
import { openDB } from "idb";

async function initDB() {
  return openDB("offlineAuthDB", 1, {
    upgrade(db) {
      db.createObjectStore("users", { keyPath: "username" });
    },
  });
}
```

---

## 2. Argon2id Derivation

```js
import * as argon2 from "argon2-browser";

async function deriveKey(password, salt) {
  const result = await argon2.hash({
    pass: password,
    salt,
    type: argon2.ArgonType.Argon2id,
    hashLen: 32,
    time: 3,
    mem: 65536,
    parallelism: 1,
  });
  return result.hash; // Uint8Array
}
```

---

## 3. AES Helpers

```js
async function aesEncrypt(keyRaw, data) {
  const key = await crypto.subtle.importKey("raw", keyRaw, "AES-GCM", false, ["encrypt"]);
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const enc = new TextEncoder().encode(data);

  const cipher = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc);
  return { iv: Array.from(iv), cipher: Array.from(new Uint8Array(cipher)) };
}

async function aesDecrypt(keyRaw, encrypted) {
  const key = await crypto.subtle.importKey("raw", keyRaw, "AES-GCM", false, ["decrypt"]);
  const iv = new Uint8Array(encrypted.iv);
  const cipher = new Uint8Array(encrypted.cipher);

  const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
  return new TextDecoder().decode(plain);
}
```

---

## 4. First Online Login (storing data)

```js
async function firstLogin(username, password, jwt, passwordHashFromServer, totpSecret = null) {
  const db = await initDB();

  // Generate per-user salt
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const derivedKey = await deriveKey(password, salt);

  // Generate a random device key (per-user)
  const deviceKey = crypto.getRandomValues(new Uint8Array(32));

  // Encrypt device key with derivedKey
  const deviceKeyEncrypted = await aesEncrypt(derivedKey, btoa(String.fromCharCode(...deviceKey)));

  // Encrypt user data with deviceKey
  const jwtEnc = await aesEncrypt(deviceKey, jwt);
  const hashEnc = await aesEncrypt(deviceKey, passwordHashFromServer);
  const totpEnc = totpSecret ? await aesEncrypt(deviceKey, totpSecret) : null;

  // Save everything under this user
  await db.put("users", {
    username,
    salt: Array.from(salt),
    deviceKeyEncrypted,
    jwtEncrypted: jwtEnc,
    hashEncrypted: hashEnc,
    totpEncrypted: totpEnc,
  });
}
```

---

## 5. Offline Login (with optional MFA)

```js
import bcrypt from "bcryptjs";
import { totp } from "otplib";

async function offlineLogin(username, password, mfaCode = null) {
  const db = await initDB();
  const user = await db.get("users", username);

  if (!user) throw new Error("No offline record for user");

  // Derive key from password
  const salt = new Uint8Array(user.salt);
  const derivedKey = await deriveKey(password, salt);

  // Decrypt device key
  const deviceKeyRaw = await aesDecrypt(derivedKey, user.deviceKeyEncrypted);
  const deviceKey = Uint8Array.from(atob(deviceKeyRaw), c => c.charCodeAt(0));

  // Decrypt stored hash + JWT
  const storedHash = await aesDecrypt(deviceKey, user.hashEncrypted);
  const jwt = await aesDecrypt(deviceKey, user.jwtEncrypted);

  // Verify password
  const validPassword = await bcrypt.compare(password, storedHash);
  if (!validPassword) throw new Error("Invalid password");

  // If TOTP secret exists, require MFA
  if (user.totpEncrypted) {
    if (!mfaCode) throw new Error("MFA code required");
    const totpSecret = await aesDecrypt(deviceKey, user.totpEncrypted);
    const isValid = totp.check(mfaCode, totpSecret);
    if (!isValid) throw new Error("Invalid MFA code");
  }

  // ✅ Success
  return { jwt };
}
```

---

## 6. Logout / Revoke User

```js
async function logoutUser(username) {
  const db = await initDB();
  await db.delete("users", username);
}
```

---

## 🔄 Flow Summary

- **First login (online)**  
  - Server validates password (+ MFA if enabled).  
  - Client generates per-user salt + device key.  
  - Device key is encrypted with password-derived key.  
  - JWT, password hash, and TOTP secret are encrypted with the device key and stored.

- **Offline login**  
  - User provides password (and MFA if enrolled).  
  - Password derives key → unlocks device key.  
  - Device key decrypts stored hash + JWT + TOTP secret.  
  - Password is verified against stored hash.  
  - If TOTP enrolled, code is verified offline.  
  - Success = session granted (reuse JWT claims).

- **Logout**  
  - Removes user entry from IndexedDB, wiping all local secrets.

---

## ⚖️ Security Properties

- Each user has a **unique device key**.  
- Device keys are wrapped with Argon2id-derived keys (resistant to brute force).  
- JWTs, password hashes, and TOTP secrets are encrypted at rest.  
- Offline MFA supported.  
- Multi-user safe: compromise of one user’s record does not expose others.  

---
