# Biometrics and the keystore - JavaScript bridge API

> Fingerprint and face unlock, and hardware-backed keys the page can encrypt with but never read.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/security

### `AppMintKeystore.decrypt`

```js
window.AppMintKeystore.decrypt(alias, base64)
```

**Example**

Decrypts data made by `AppMintKeystore.encrypt` with the same key.

**Returns:** an object, at once (not a Promise): `{ok:true, base64}` (the plain data) or `{ok:false, error, detail}`. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function fromBase64(b64) { return decodeURIComponent(escape(atob(b64))); }

function readSecret() {
  var sealed = localStorage.getItem('secret');
  if (!sealed || !window.AppMintKeystore) return null;

  var r = window.AppMintKeystore.decrypt('vault', sealed);
  if (r.ok) return fromBase64(r.base64);

  switch (r.error) {
    case 'auth-required':   unlockThen(readSecretAndShow); break;   // unlock, then try again
    case 'no-such-key':
    case 'key-invalidated': alert('This secret can no longer be opened.'); break;
    default:                alert('Could not decrypt: ' + r.error);
  }
  return null;
}
```

For a file written with `WebToApkFS.writeSealed`, read it back with `WebToApkFS.readSealed(path, alias)` - same result shape.

Read back a sealed JSON object, unlocking first when the key needs it:

```js
function unlock() {
  return new Promise(function (resolve) {
    var id = 'unlock_' + Date.now();
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
    window.WebToApk.authenticateBiometricEx(id, JSON.stringify({
      title: 'Unlock your settings', strong: true, allowDeviceCredential: true
    }));
  });
}

async function loadSealedSettings() {
  var sealed = localStorage.getItem('settings');
  if (!sealed || !window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return null;
  var r = window.AppMintKeystore.decrypt('vault', sealed);
  if (!r.ok && r.error === 'auth-required' && (await unlock()).ok) {
    r = window.AppMintKeystore.decrypt('vault', sealed);
  }
  return r.ok ? JSON.parse(decodeURIComponent(escape(atob(r.base64)))) : null;
}
```

Open a list of sealed diary entries, keeping the ones that fail visible as "locked":

```js
function loadDiary() {
  if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return [];
  var entries = JSON.parse(localStorage.getItem('diary') || '[]');   // [{date, sealed}]
  return entries.map(function (e) {
    var r = window.AppMintKeystore.decrypt('diary', e.sealed);
    return {
      date: e.date,
      text: r.ok ? decodeURIComponent(escape(atob(r.base64))) : null,
      error: r.ok ? null : r.error                 // e.g. 'auth-required', 'no-such-key'
    };
  });
}
```

**Notes:** After `deleteKey` the data is gone for good; that is the point. A payload that was changed fails the GCM check. Other errors: `bad base64`, `disabled`, `foreign_origin`, `unsupported`.

### `AppMintKeystore.deleteKey`

```js
window.AppMintKeystore.deleteKey(alias)
```

**Example**

Destroys a secure key permanently. Everything encrypted with it can never be read again - by anyone.

**Returns:** `true` (deleted) or `false` (no such key, or the feature is off), at once. **Needs:** **Secure Keys (encryption)** turned on when you build.

Overwriting a file does not erase it on flash memory. Encrypt it, then destroy the key - that is a real delete:

```js
function deleteNoteForReal() {
  if (!window.AppMintKeystore || !window.WebToApkFS) return;
  if (!confirm('Delete this note forever?')) return;

  var gone = window.AppMintKeystore.deleteKey('vault');  // secret.bin is unreadable from now on
  window.WebToApkFS.delete('secret.bin');                // tidy up the file too
  alert(gone ? 'Deleted.' : 'Nothing to delete.');
}
```

"Erase everything" in settings: destroy every key this app made, then clear the page's storage:

```js
function eraseEverything() {
  var K = window.AppMintKeystore;
  if (!K || !K.isAvailable()) return;
  if (!confirm('Erase all protected data on this phone? This cannot be undone.')) return;

  var aliases = K.listKeys();
  aliases.forEach(function (alias) { K.deleteKey(alias); });
  localStorage.clear();
  alert('Erased ' + aliases.length + ' key(s).');
  location.reload();
}
```

Self-destruct the vault after too many wrong app PINs:

```js
var MAX_TRIES = 5;

function onWrongPin() {
  var tries = Number(localStorage.getItem('pinTries') || 0) + 1;
  localStorage.setItem('pinTries', String(tries));
  if (tries < MAX_TRIES) return alert((MAX_TRIES - tries) + ' tries left.');

  if (window.AppMintKeystore) window.AppMintKeystore.deleteKey('vault');  // vault unreadable from now on
  localStorage.removeItem('vaultData');
  localStorage.removeItem('pinTries');
  alert('Too many wrong PINs. The vault was erased.');
}
```

**Notes:** There is no undo and no recovery. If `isHardwareBacked(alias)` was `false`, the key lived in software - say so before you promise the user an irreversible delete.

### `AppMintKeystore.encrypt`

```js
window.AppMintKeystore.encrypt(alias, base64)
```

**Example**

Encrypts base64 data with a secure key. Store the result anywhere - it is useless without the key.

**Returns:** an object, at once (not a Promise): `{ok:true, base64}` (IV + ciphertext + tag) or `{ok:false, error, detail}`. **Needs:** **Secure Keys (encryption)** turned on when you build, and a key from `AppMintKeystore.generateKey`.

```js
function toBase64(text) { return btoa(unescape(encodeURIComponent(text))); }

function saveSecret(text) {
  if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return false;
  var r = window.AppMintKeystore.encrypt('vault', toBase64(text));
  if (r.ok) {
    localStorage.setItem('secret', r.base64);
    return true;
  }
  if (r.error === 'auth-required') {
    // the key needs a fresh unlock: ask for the fingerprint, then call saveSecret again
    unlockThen(function () { saveSecret(text); });
  } else {
    alert('Could not encrypt: ' + r.error);
  }
  return false;
}
```

`unlockThen` can be built on `authenticateBiometricEx` (see that example).

To write an encrypted FILE of any size, use `WebToApkFS.writeSealed(path, base64, alias)` - it encrypts chunk by chunk with this same key.

Seal a small JSON object (a note, a token, settings) and keep it in localStorage:

```js
function sealJson(alias, obj) {
  if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return null;
  var b64 = btoa(unescape(encodeURIComponent(JSON.stringify(obj))));
  var r = window.AppMintKeystore.encrypt(alias, b64);
  if (!r.ok) { console.warn('encrypt failed:', r.error, r.detail); return null; }
  return r.base64;
}

var sealed = sealJson('notes', { title: 'Shopping', items: ['milk', 'eggs'] });
if (sealed) localStorage.setItem('note-1', sealed);
```

Create the key the first time it is needed, then encrypt:

```js
function encryptWithKeyReady(alias, b64) {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return { ok: false, error: 'unsupported' };
  var r = ks.encrypt(alias, b64);
  if (!r.ok && r.error === 'no-such-key') {
    var g = ks.generateKey(alias);          // plain key, no unlock needed
    if (!g.ok) return g;
    r = ks.encrypt(alias, b64);
  }
  return r;                                 // {ok:true, base64} or {ok:false, error, detail}
}
```

**Notes:** Max 4 MB per call (`chunk too large`). Errors: `no-such-key`, `auth-required`, `key-invalidated` (the screen lock changed - the key is gone), `bad base64`, `disabled`, `foreign_origin`, `unsupported`.

### `AppMintKeystore.generateKey`

```js
window.AppMintKeystore.generateKey(alias, options)
```

**Example**

Creates (or replaces) an AES-256-GCM key in the Android Keystore. The key never leaves secure hardware; your page only uses its name.

**Returns:** an object, at once (not a Promise): `{ok:true, alias, hardwareBacked}` or `{ok:false, error, detail}`. **Needs:** **Secure Keys (encryption)** turned on when you build (see `AppMintKeystore.isAvailable`).

```js
function createVault() {
  if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) {
    alert('Secure keys are not available in this app.');
    return;
  }
  var r = window.AppMintKeystore.generateKey('vault', {
    requireAuth: true,         // usable only after a fingerprint / PIN unlock...
    authValiditySeconds: 30    // ...for 30 seconds after it
  });
  if (!r.ok) { alert('Could not create the key: ' + r.error); return; }
  console.log('Hardware key:', r.hardwareBacked);
}
```

A plain key, no unlock needed:

```js
var r = window.AppMintKeystore.generateKey('notes');   // options default to {}
```

Demand the separate secure chip (StrongBox). It fails instead of giving you a weaker key:

```js
var r = window.AppMintKeystore.generateKey('wallet', { strongBox: true });
if (!r.ok && r.error === 'strongbox-unavailable') {
  alert('This phone has no secure chip.');
}
```

**Notes:** Alias: letters, digits, `_ - .`, up to 64 characters (`error:"bad alias"` otherwise). Using an existing alias replaces the key, and data sealed with the old one is lost. Other errors: `disabled`, `bad options json`, `foreign_origin`, `unsupported` (no bridge).

### `AppMintKeystore.hasKey`

```js
window.AppMintKeystore.hasKey(alias)
```

**Example**

Tells you whether a secure key with this alias exists.

**Returns:** `true` / `false`, at once. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function startVault() {
  if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return;
  if (window.AppMintKeystore.hasKey('vault')) {
    showUnlockScreen();
  } else {
    showCreateVaultScreen();
  }
}
window.addEventListener('load', startVault);
```

Make sure a key exists before the first save, creating it only once:

```js
function ensureKey(alias) {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return false;
  if (ks.hasKey(alias)) return true;
  var r = ks.generateKey(alias);   // generateKey REPLACES an existing key, so only call it when hasKey says no
  if (!r.ok) console.warn('Could not create key:', r.error);
  return r.ok;
}

if (ensureKey('notes')) enableSaveButton();
```

Clean up sealed data whose key is gone (deleted, or the app data was restored without it):

```js
function checkVault() {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return;
  var sealed = localStorage.getItem('secret');
  if (sealed && !ks.hasKey('vault')) {
    localStorage.removeItem('secret');   // without the key it can never be opened
    showMessage('Your saved secret could not be recovered and was removed.');
  }
}
window.addEventListener('load', checkVault);
```

**Notes:** Answers `false` when Secure Keys is off, so check `isAvailable()` first if you need to tell "no key yet" from "feature not in this app".

### `AppMintKeystore.isAvailable`

```js
window.AppMintKeystore.isAvailable()
```

**Example**

Tells you whether hardware-backed secure keys can be used in this app.

**Returns:** `true` / `false`, at once (not a Promise). **Needs:** **Secure Keys (encryption)** turned on when you build. The `window.AppMintKeystore` object is installed (before your first script runs) whenever **Secure Keys** or **Native Folder Access (SAF)** is on; `isAvailable()` answers for Secure Keys.

```js
function secureKeysReady() {
  return !!window.AppMintKeystore && window.AppMintKeystore.isAvailable();
}

// Check when the user acts (the helper is added when the page finishes loading):
document.getElementById('vault-button').addEventListener('click', function () {
  if (!secureKeysReady()) {
    alert('The secure vault is not available in this app.');
    return;
  }
  openVault();
});
```

Hide every secure-vault control when the feature is not in this app:

```js
window.addEventListener('load', function () {
  var ok = !!window.AppMintKeystore && window.AppMintKeystore.isAvailable();
  document.querySelectorAll('.needs-secure-keys').forEach(function (el) {
    el.hidden = !ok;
  });
});
```

Tell "feature off" apart from "no key yet" when you decide which screen to show:

```js
function vaultState() {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return 'off';   // a browser, or Secure Keys not on in this build
  return ks.hasKey('vault') ? 'ready' : 'new';
}

document.getElementById('vault-button').addEventListener('click', function () {
  var state = vaultState();
  if (state === 'off') alert('The secure vault is not available in this app.');
  else if (state === 'new') showCreateVaultScreen();
  else showUnlockScreen();
});
```

**Notes:** The helper is added after the page has loaded, so do not test for it in a script that runs while the page is still parsing - test in a click handler or after `load`. In a browser there is no `window.AppMintKeystore`; the check above returns `false` without throwing.

### `AppMintKeystore.isHardwareBacked`

```js
window.AppMintKeystore.isHardwareBacked(alias)
```

**Example**

Tells you whether a key lives in the phone's secure hardware (TEE or StrongBox) rather than in software.

**Returns:** `true` / `false`, at once. `false` when the key does not exist. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function showKeyStrength() {
  if (!window.AppMintKeystore || !window.AppMintKeystore.hasKey('vault')) return;
  var hw = window.AppMintKeystore.isHardwareBacked('vault');
  document.getElementById('strength').textContent = hw
    ? 'Protected by secure hardware'
    : 'Protected by software (this phone has no secure hardware for keys)';
}
```

Only keep a high-value secret (a wallet seed, a recovery code) when its key is in secure hardware:

```js
function walletKeyIsHardware() {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return false;
  if (!ks.hasKey('wallet')) {
    var r = ks.generateKey('wallet', { requireAuth: true });
    if (!r.ok) return false;
  }
  return ks.isHardwareBacked('wallet');
}

if (!walletKeyIsHardware()) {
  alert('This phone cannot keep the wallet key in secure hardware.');
}
```

List where every key lives, for a support report:

```js
function keyReport() {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return { secureKeys: false };
  return {
    secureKeys: true,
    keys: ks.listKeys().map(function (alias) {
      return { alias: alias, hardware: ks.isHardwareBacked(alias) };
    })
  };
}
console.log(JSON.stringify(keyReport()));
```

**Notes:** `generateKey` also returns `hardwareBacked` right after it creates the key.

### `AppMintKeystore.listKeys`

```js
window.AppMintKeystore.listKeys()
```

**Example**

Lists the aliases of every secure key your pages created.

**Returns:** an array of strings, sorted, at once, e.g. `['notes', 'vault']`. `[]` when there are none or the feature is off. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function renderKeyList() {
  var list = document.getElementById('key-list');
  list.innerHTML = '';
  if (!window.AppMintKeystore) return;
  window.AppMintKeystore.listKeys().forEach(function (alias) {
    var li = document.createElement('li');
    li.textContent = alias + (window.AppMintKeystore.isHardwareBacked(alias) ? ' (hardware)' : '');
    list.appendChild(li);
  });
}
```

Destroy every key on "Erase all my data", which makes everything sealed with them unreadable:

```js
function eraseAllKeys() {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return 0;
  var removed = 0;
  ks.listKeys().forEach(function (alias) {
    if (ks.deleteKey(alias)) removed++;
  });
  localStorage.clear();   // the sealed copies can no longer be opened anyway
  return removed;
}
```

Keep one key per signed-in user and remove the keys of users who signed out:

```js
function pruneUserKeys(currentUserId) {
  var ks = window.AppMintKeystore;
  if (!ks || !ks.isAvailable()) return;
  var keep = 'user-' + currentUserId;   // aliases: letters, digits, _ - . only
  ks.listKeys().forEach(function (alias) {
    if (alias.indexOf('user-') === 0 && alias !== keep) ks.deleteKey(alias);
  });
}
```

**Notes:** Only keys made through the bridge are listed; the app's own internal keys are never shown.

### `authenticateBiometric`

```js
window.WebToApk.authenticateBiometric(callbackId: String, title: String, subtitle: String)
```

WebToApk.isBiometricAvailable()            -> "available" | "no_hardware"                                                | "not_enrolled" | "unavailable"   WebToApk.authenticateBiometric(id, title, subtitle) Result is delivered to the same __webToApkAuth callback table the Google Sign-In bridge uses:   window.__webToApkAuth = window.__webToApkAuth || {};   window.__webToApkAuth['unlock'] = function (json) {       var r = JSON.parse(json);      // {ok:true} | {ok:false,error:"..."}       if (r.ok) showApp();   };   WebToApk.authenticateBiometric('unlock', 'Unlock', 'Use your fingerprint');

_Described by its group, Fingerprint / biometric bridge, rather than on its own._

**Example**

Shows the system fingerprint prompt with a title and subtitle. The simple form of `authenticateBiometricEx` (use that one for the PIN option, a custom Cancel label or "strong" biometrics).

**Returns:** nothing. The answer arrives later: the shell calls `window.__webToApkAuth[callbackId](json)` with `'{"ok":true}'` or `'{"ok":false,"error":"<code>"}'`, then removes that entry. **Needs:** **Fingerprint** ticked when you build (without it: `error:"permission_missing"`).

Register the callback FIRST, then call:

```js
function unlockWithFingerprint() {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof window.WebToApk.authenticateBiometric !== 'function') {
      resolve({ ok: false, error: 'not_in_app' });
      return;
    }
    var id = 'bio_' + Date.now();
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
    window.WebToApk.authenticateBiometric(id, 'Unlock', 'Touch the fingerprint sensor');
  });
}

document.getElementById('unlock').addEventListener('click', async function () {
  var r = await unlockWithFingerprint();
  if (r.ok) showMyApp();
  else if (r.error === 'user_cancel') { /* the user closed it — do nothing */ }
  else if (r.error === 'none_enrolled') alert('Please add a fingerprint in phone settings.');
  else if (r.error === 'lockout') alert('Too many tries. Wait a little and try again.');
  else alert('Could not unlock: ' + r.error);
});
```

Confirm a risky action with the fingerprint first (an empty title shows the app's name):

```js
function confirmItsYou(subtitle) {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof window.WebToApk.authenticateBiometric !== 'function') {
      resolve({ ok: false, error: 'not_in_app' });
      return;
    }
    var id = 'confirm_' + Date.now();
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
    window.WebToApk.authenticateBiometric(id, '', subtitle);
  });
}

document.getElementById('delete-account').addEventListener('click', async function () {
  var r = await confirmItsYou('Confirm to delete your account');
  if (r.ok) deleteMyAccount();
  else if (r.error !== 'user_cancel') alert('Not confirmed: ' + r.error);
});
```

Lock the app again when it comes back after more than a minute in the background:

```js
var hiddenAt = 0;

document.addEventListener('visibilitychange', async function () {
  if (document.visibilityState === 'hidden') {
    hiddenAt = Date.now();
    return;
  }
  if (!hiddenAt || Date.now() - hiddenAt < 60000) return;
  hiddenAt = 0;
  hideMyApp();
  var r = await unlockWithFingerprint();     // from the first example
  if (r.ok) showMyApp();
  else showLockedScreen();                   // with its own "Unlock" button
});
```

**Notes:** An empty title uses the app's name. A wrong finger does NOT end the prompt - the callback fires only on success, cancel or a real error. Error codes are the same as `authenticateBiometricEx`. This proves the phone's owner is present; it does not prove who they are to a server.

### `authenticateBiometricEx`

```js
window.WebToApk.authenticateBiometricEx(callbackId: String, optionsJson: String)
```

The full form. [optionsJson] accepts: title, subtitle, description, negativeButtonText, allowDeviceCredential (PIN/pattern/password fallback), strong (require BIOMETRIC_STRONG - use for anything payment-shaped). Errors are distinct codes, not one boolean. Before any prompt: no_hardware | none_enrolled | unavailable (the status check could not run - see isBiometricAvailable). From the Android 9+ system prompt: user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error, and for a code Android adds later its own message text (or `error_<code>`). permission_missing when the build lacks the Fingerprint permission. On Android 7-8 (API 24-27, fingerprint only) an error is the phone's own message text as the OEM wrote it - not a fixed code.

**Example**

Shows the system fingerprint / face prompt with full options: PIN or pattern as a second way in, a custom Cancel label, and "strong" biometrics only.

**Returns:** nothing. The answer arrives later: the shell calls `window.__webToApkAuth[callbackId](json)` with `'{"ok":true}'` or `'{"ok":false,"error":"<code>"}'`, then removes that entry. **Needs:** **Fingerprint** ticked when you build (without it: `error:"permission_missing"`).

A small Promise wrapper you can reuse:

```js
function verifyUser(options) {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof window.WebToApk.authenticateBiometricEx !== 'function') {
      resolve({ ok: false, error: 'not_in_app' });
      return;
    }
    var id = 'bio_' + Date.now() + '_' + Math.random().toString(36).slice(2);
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
    window.WebToApk.authenticateBiometricEx(id, JSON.stringify(options));
  });
}
```

Unlock the app - fingerprint, face, or the phone's PIN/pattern:

```js
async function unlockApp() {
  var r = await verifyUser({
    title: 'Unlock',
    subtitle: 'Use your fingerprint or PIN',
    allowDeviceCredential: true       // Android 11+: the system adds "Use PIN"
  });
  if (r.ok) showMyApp();
  else if (r.error === 'lockout') alert('Too many tries. Wait a little and try again.');
}
```

Confirm a payment - strong biometrics only, your own Cancel label:

```js
async function confirmPayment() {
  var r = await verifyUser({
    title: 'Confirm payment',
    description: 'Pay 12.00 to Coffee House',
    negativeButtonText: 'Not now',
    strong: true                      // BIOMETRIC_STRONG (Android 11+)
  });
  if (r.ok) sendPayment();
  else if (r.error !== 'user_cancel') alert('Could not confirm: ' + r.error);
}
```

Unlock a Keystore key made with `requireAuth`, then retry:

```js
async function openVault() {
  var r = await verifyUser({ title: 'Open vault', allowDeviceCredential: true });
  if (r.ok) readSecret();    // AppMintKeystore.decrypt works for authValiditySeconds now
}
```

**Notes:** Error codes: `user_cancel`, `none_enrolled`, `no_hardware`, `hw_unavailable`, `lockout` (wait and retry), `lockout_permanent` (the user must unlock the phone with PIN first), `no_device_credential`, `timeout`, `permission_missing`, `unavailable`, and rarely `no_space`, `unable_to_process`, `vendor_error`. On Android 7-8 the shell shows its own dialog and an error is the system's message text. `allowDeviceCredential` and `negativeButtonText` do not mix: with the PIN option the system draws its own button. A wrong finger does not end the prompt.

### `decryptWithKey`

```js
window.WebToApk.decryptWithKey(alias: String, base64Payload: String): String
```

Reverses [encryptWithKey]. Fails with `error:"no-such-key"` once the key has been deleted - the payload is then unrecoverable by design. @return JSON `{ok, base64, error}`.

**Example**

Decrypts data made by `encryptWithKey` with the same Keystore key. Most pages use `AppMintKeystore.decrypt(alias, base64)`, which returns the parsed object.

**Returns:** a JSON string, at once: `{"ok":true,"base64":"<plain data>"}` or `{"ok":false,"error":"...","detail":"..."}`. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function fromBase64(b64) { return decodeURIComponent(escape(atob(b64))); }

function openNote() {
  if (!window.WebToApk || typeof window.WebToApk.decryptWithKey !== 'function') return null;
  var sealed = localStorage.getItem('note');
  if (!sealed) return null;
  var r = JSON.parse(window.WebToApk.decryptWithKey('vault', sealed));
  if (r.ok) return fromBase64(r.base64);

  if (r.error === 'auth-required') {
    askUserToUnlockThenRetry();          // unlock with authenticateBiometricEx, then call again
  } else if (r.error === 'no-such-key' || r.error === 'key-invalidated') {
    alert('This note can no longer be opened.');   // the key was deleted or the screen lock changed
  } else {
    alert('Could not open the note: ' + r.error);
  }
  return null;
}
```

For a key made with `requireAuth:true`: on `auth-required`, show the unlock prompt, then decrypt again:

```js
function unlock() {
  return new Promise(function (resolve) {
    var id = 'unlock_' + Date.now();
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
    window.WebToApk.authenticateBiometricEx(id, JSON.stringify({
      title: 'Unlock your notes', strong: true, allowDeviceCredential: true
    }));
  });
}

async function openNoteWithUnlock() {
  if (!window.WebToApk || typeof window.WebToApk.decryptWithKey !== 'function') return null;
  var sealed = localStorage.getItem('note');
  if (!sealed) return null;
  var r = JSON.parse(window.WebToApk.decryptWithKey('vault', sealed));
  if (!r.ok && r.error === 'auth-required') {
    var auth = await unlock();
    if (!auth.ok) return null;                      // cancelled or failed
    r = JSON.parse(window.WebToApk.decryptWithKey('vault', sealed));
  }
  return r.ok ? decodeURIComponent(escape(atob(r.base64))) : null;
}
```

Show an encrypted photo: decrypt to bytes, then display it through a blob URL:

```js
function showSealedPhoto(sealedB64, img) {
  if (!window.WebToApk || typeof window.WebToApk.decryptWithKey !== 'function') return;
  var r = JSON.parse(window.WebToApk.decryptWithKey('vault', sealedB64));
  if (!r.ok) {
    alert('Could not open the photo: ' + r.error);
    return;
  }
  var bin = atob(r.base64);
  var bytes = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  var url = URL.createObjectURL(new Blob([bytes], { type: 'image/jpeg' }));
  img.onload = function () { URL.revokeObjectURL(url); };
  img.src = url;
}
```

**Notes:** Once the key is deleted (`deleteKey`) the data can never be decrypted again - by design. A changed or damaged payload fails (the GCM tag does not match); the `error` is then the platform's message, not one of the codes above. Other codes: `bad base64`, `disabled`, `foreign_origin`.

### `deleteKey`

```js
window.WebToApk.deleteKey(alias: String): String
```

Destroys [alias] permanently. Everything encrypted with it becomes undecryptable - including by this app, and by anyone holding the device.

**Example**

Destroys a Keystore key for good. Everything encrypted with it can never be read again - by you, the user or anyone holding the phone. Most pages use `AppMintKeystore.deleteKey(alias)`, which returns a real boolean.

**Returns:** the STRING `"true"` (deleted) or `"false"` (no such key, Secure Keys off, or not the app's own page), at once. **Needs:** **Secure Keys (encryption)** turned on when you build.

This is the only real "secure delete" on a phone: overwriting a file does not erase it on flash memory, destroying the key does.

```js
function destroyVault() {
  if (!window.WebToApk || typeof window.WebToApk.deleteKey !== 'function') return;
  if (!confirm('Delete the vault? This cannot be undone.')) return;

  var gone = window.WebToApk.deleteKey('vault') === 'true';
  localStorage.removeItem('note');              // the sealed data is now useless anyway
  alert(gone ? 'Vault deleted.' : 'There was no vault to delete.');
}
```

Rotate a key: open the data with the old key, seal it with a new one, then destroy the old key:

```js
function rotateVault() {
  var W = window.WebToApk;
  if (!W || typeof W.deleteKey !== 'function') return false;
  var sealed = localStorage.getItem('note');
  if (!sealed) return false;

  var clear = JSON.parse(W.decryptWithKey('vault', sealed));
  if (!clear.ok) return false;
  var made = JSON.parse(W.generateKey('vault2', '{}'));
  if (!made.ok) return false;
  var fresh = JSON.parse(W.encryptWithKey('vault2', clear.base64));
  if (!fresh.ok) return false;

  localStorage.setItem('note', fresh.base64);
  localStorage.setItem('noteKey', 'vault2');
  return W.deleteKey('vault') === 'true';          // the old copies are dead from here on
}
```

"Forget me on this phone" at sign-out: destroy the account's key, then drop its cached data:

```js
function signOutAndForget(userId) {
  var alias = 'user_' + userId;
  if (window.WebToApk && typeof window.WebToApk.deleteKey === 'function') {
    var gone = window.WebToApk.deleteKey(alias) === 'true';
    console.log(gone ? 'Account key destroyed' : 'No key for this account');
  }
  localStorage.removeItem('messages_' + userId);   // sealed with that key, unreadable now
  signOut();
}
```

**Notes:** Check `isKeyHardwareBacked` before you promise a user that deletion is irreversible - a software-only key is weaker.

### `encryptWithKey`

```js
window.WebToApk.encryptWithKey(alias: String, base64Plaintext: String): String
```

Encrypts base64 [base64Plaintext] with [alias]. Each call generates its own IV, so a large file is encrypted chunk by chunk - chunks are independent and may be decrypted in any order. @return JSON `{ok, base64, error}` where `base64` is `IV || ciphertext || tag`.

**Example**

Encrypts base64 data with a Keystore key. Most pages use `AppMintKeystore.encrypt(alias, base64)`, which returns the parsed object.

**Returns:** a JSON string, at once: `{"ok":true,"base64":"<IV + ciphertext + tag>"}` or `{"ok":false,"error":"...","detail":"..."}`. **Needs:** **Secure Keys (encryption)** turned on when you build, and a key made with `generateKey`.

The input is base64. For text, turn it into UTF-8 base64 first:

```js
function toBase64(text) { return btoa(unescape(encodeURIComponent(text))); }

function sealNote(text) {
  if (!window.WebToApk || typeof window.WebToApk.encryptWithKey !== 'function') return null;
  var r = JSON.parse(window.WebToApk.encryptWithKey('vault', toBase64(text)));
  if (r.ok) {
    localStorage.setItem('note', r.base64);   // safe to store anywhere: useless without the key
    return r.base64;
  }
  if (r.error === 'auth-required') {
    // key was made with requireAuth — unlock first (authenticateBiometricEx), then call again
    askUserToUnlockThenRetry();
  } else {
    alert('Could not encrypt: ' + r.error);
  }
  return null;
}
```

Seal a whole object (a session, some settings) as JSON before you store it:

```js
function sealJson(alias, obj) {
  if (!window.WebToApk || typeof window.WebToApk.encryptWithKey !== 'function') return null;
  var b64 = btoa(unescape(encodeURIComponent(JSON.stringify(obj))));
  var r = JSON.parse(window.WebToApk.encryptWithKey(alias, b64));
  if (!r.ok) console.warn('encrypt failed: ' + r.error);
  return r.ok ? r.base64 : null;
}

var sealed = sealJson('vault', { token: accessToken, savedAt: Date.now() });
if (sealed) localStorage.setItem('session', sealed);
```

Encrypt a photo the user picks (binary data, up to 4 MB per call):

```js
document.getElementById('id-photo').addEventListener('change', async function (e) {
  var file = e.target.files[0];
  if (!file || !window.WebToApk || typeof window.WebToApk.encryptWithKey !== 'function') return;
  var bytes = new Uint8Array(await file.arrayBuffer());
  var bin = '';
  for (var i = 0; i < bytes.length; i += 0x8000) {
    bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  }
  var r = JSON.parse(window.WebToApk.encryptWithKey('vault', btoa(bin)));
  if (r.ok) {
    saveSealedPhoto(file.name, r.base64);          // your own storage
  } else if (r.error.indexOf('chunk too large') === 0) {
    alert('That photo is too big. Pick one under 4 MB.');
  } else {
    alert('Could not encrypt the photo: ' + r.error);
  }
});
```

**Notes:** Each call uses a fresh IV, so the same text gives a different result every time. At most 4 MB per call (`error:"chunk too large ..."`); for bigger files encrypt chunk by chunk, or use `WebToApkFS.writeSealed`. Error codes: `no-such-key`, `auth-required`, `key-invalidated` (the user changed the screen lock - the key is gone for good), `bad base64`, `disabled`, `foreign_origin`.

### `generateKey`

```js
window.WebToApk.generateKey(alias: String, optionsJson: String): String
```

Creates (or replaces) an AES-256-GCM key called [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` binds the key to the device lock; `strongBox` fails outright on devices without a secure element rather than handing back a weaker key than was asked for. @return JSON `{ok, alias, hardwareBacked, error}`.

**Example**

Creates (or replaces) an AES-256-GCM key in the Android Keystore. The page gets a name for the key, never the key itself. Most pages use `AppMintKeystore.generateKey(alias, options)`, which does the JSON work for you.

**Returns:** a JSON string, at once: `{"ok":true,"alias":"vault","hardwareBacked":true}` or `{"ok":false,"error":"...","detail":"..."}`. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function createVaultKey() {
  if (!window.WebToApk || typeof window.WebToApk.generateKey !== 'function') {
    alert('Secure keys work only in the app.');
    return false;
  }
  var options = {
    requireAuth: true,          // key works only after the user unlocks (fingerprint / PIN)
    authValiditySeconds: 30,    // ...and for 30 seconds after that unlock
    strongBox: false            // true = demand the separate secure chip (fails if missing)
  };
  var r = JSON.parse(window.WebToApk.generateKey('vault', JSON.stringify(options)));
  if (!r.ok) {
    alert('Could not create the key: ' + r.error);
    return false;
  }
  if (!r.hardwareBacked) console.log('Key is kept in software on this phone');
  return true;
}
```

A plain key with no unlock rule (options may be `'{}'`):

```js
var r = JSON.parse(window.WebToApk.generateKey('notes', '{}'));
```

Create the key once, the first time it is needed - calling `generateKey` again would replace it and orphan everything already sealed:

```js
function ensureKey(alias) {
  if (!window.WebToApk || typeof window.WebToApk.generateKey !== 'function') return false;
  if (window.WebToApk.hasKey(alias) === 'true') return true;    // keep the existing key
  var r = JSON.parse(window.WebToApk.generateKey(alias, '{}'));
  if (!r.ok && r.error === 'disabled') {
    console.warn('Secure Keys was not turned on for this build');
  }
  return r.ok;
}

if (ensureKey('notes')) enableEncryptedNotes();
```

Demand the separate secure chip for a wallet key, and tell the user plainly when the phone does not have one:

```js
function createWalletKey() {
  if (!window.WebToApk || typeof window.WebToApk.generateKey !== 'function') return false;
  var r = JSON.parse(window.WebToApk.generateKey('wallet', JSON.stringify({
    requireAuth: true,
    authValiditySeconds: 10,
    strongBox: true
  })));
  if (r.ok) return true;
  if (r.error === 'strongbox-unavailable') {
    alert('The wallet needs a phone with a dedicated security chip.');
  } else if (r.error === 'bad alias') {
    alert('Key names may use letters, digits, _ - . only.');
  } else {
    alert('Could not create the wallet key: ' + r.error);
  }
  return false;
}
```

**Notes:** The alias may use only letters, digits, `_`, `-`, `.` and at most 64 characters, otherwise `error:"bad alias"`. Calling it again with the same alias REPLACES the key, and data sealed with the old key can no longer be read. Error codes: `disabled` (Secure Keys not turned on), `bad options json`, `strongbox-unavailable` (you asked for `strongBox:true` and the phone has no secure chip, or it is older than Android 9), `foreign_origin` (called from a page that is not the app's own).

### `hasKey`

```js
window.WebToApk.hasKey(alias: String): String
```

True when a key called [alias] exists in this app's keystore.

**Example**

Tells you whether a Keystore key with this alias exists. Most pages use `AppMintKeystore.hasKey(alias)`, which returns a real boolean.

**Returns:** the STRING `"true"` or `"false"`, at once. **Needs:** **Secure Keys (encryption)** turned on when you build (without it the answer is always `"false"`).

```js
function vaultExists() {
  if (!window.WebToApk || typeof window.WebToApk.hasKey !== 'function') return false;
  return window.WebToApk.hasKey('vault') === 'true';   // compare the string
}

if (!vaultExists()) {
  showCreateVaultScreen();
}
```

Pick the start screen: unlock an existing vault, or set one up - and clear sealed data whose key is gone:

```js
function chooseStartScreen() {
  if (!window.WebToApk || typeof window.WebToApk.hasKey !== 'function') return showMyApp();
  var hasVault = window.WebToApk.hasKey('vault') === 'true';
  var hasData = localStorage.getItem('note') !== null;

  if (hasVault) {
    showUnlockScreen();
  } else if (hasData) {
    localStorage.removeItem('note');          // sealed with a key that no longer exists
    showCreateVaultScreen('Your old vault could not be opened, so it was cleared.');
  } else {
    showCreateVaultScreen();
  }
}
```

One key per signed-in account, made the first time that account signs in on this phone:

```js
function keyAliasFor(userId) {
  // aliases allow letters, digits, _ - . and at most 64 characters
  return 'user_' + String(userId).replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 59);
}

function onSignedIn(user) {
  if (!window.WebToApk || typeof window.WebToApk.hasKey !== 'function') return openAccount(user, null);
  var alias = keyAliasFor(user.id);
  if (window.WebToApk.hasKey(alias) !== 'true') {
    var r = JSON.parse(window.WebToApk.generateKey(alias, '{}'));
    if (!r.ok) return alert('Could not set up secure storage: ' + r.error);
  }
  openAccount(user, alias);
}
```

**Notes:** The answer is a string, so `if (WebToApk.hasKey('vault'))` is always true - compare with `=== 'true'`. You only see keys this app created through the bridge; the shell's own keys are hidden.

### `isBiometricAvailable`

```js
window.WebToApk.isBiometricAvailable(): String
```

"available" | "no_hardware" | "not_enrolled" | "unavailable". Android 10+ counts face, iris and fingerprint; Android 6-9 the fingerprint sensor only. "unavailable" = the sensor is temporarily unusable, or the check could not run (e.g. the build did not enable the Fingerprint permission). See [BiometricStatus].

**Example**

Tells you whether the phone can do a biometric unlock (face, iris or fingerprint) right now. Check it before you show an unlock button.

**Returns:** a string, at once: `"available"`, `"no_hardware"` (no sensor), `"not_enrolled"` (sensor, but nothing registered, or no screen lock set) or `"unavailable"` (the sensor is temporarily unusable, or the check could not run - for example the build did not tick **Fingerprint**). **Needs:** **Fingerprint** ticked when you build - without it Android refuses to answer and the result is `"unavailable"`.

```js
function setupFingerprintButton() {
  var btn = document.getElementById('fingerprint-button');
  if (!window.WebToApk || typeof window.WebToApk.isBiometricAvailable !== 'function') {
    btn.hidden = true;                      // browser: no fingerprint bridge
    return;
  }
  var state = window.WebToApk.isBiometricAvailable();
  if (state === 'available') {
    btn.hidden = false;
  } else if (state === 'not_enrolled') {
    btn.hidden = true;
    showHint('Add a fingerprint in your phone settings to unlock with it.');
  } else {
    btn.hidden = true;                      // no_hardware / unavailable
  }
}
```

A settings switch "Unlock with fingerprint" that is greyed out with the reason when it cannot work:

```js
var BIO_REASONS = {
  no_hardware: 'This phone has no fingerprint or face sensor.',
  not_enrolled: 'Set up a fingerprint or face unlock in your phone settings first.',
  unavailable: 'The sensor cannot be used right now.'
};

function renderBioSwitch() {
  var sw = document.getElementById('bio-switch');
  var note = document.getElementById('bio-note');
  var inApp = window.WebToApk && typeof window.WebToApk.isBiometricAvailable === 'function';
  var state = inApp ? window.WebToApk.isBiometricAvailable() : 'not_in_app';
  sw.disabled = state !== 'available';
  if (sw.disabled) sw.checked = false;
  note.textContent = state === 'not_in_app' ? 'Available in the app.' : (BIO_REASONS[state] || '');
}
```

Check again when the user comes back from phone settings, in case they just added a fingerprint:

```js
document.addEventListener('visibilitychange', function () {
  if (document.visibilityState !== 'visible') return;
  if (!window.WebToApk || typeof window.WebToApk.isBiometricAvailable !== 'function') return;
  if (window.WebToApk.isBiometricAvailable() === 'available') {
    renderBioSwitch();
    showHint('Fingerprint unlock is ready. Turn it on in Settings.');
  }
});
```

**Notes:** On Android 10 and newer the check counts face, iris and fingerprint, so a face-unlock-only phone answers `"available"`. On Android 6-9 it looks at the fingerprint sensor only, so a face-only phone there answers `"no_hardware"`; with `allowDeviceCredential:true` in `authenticateBiometricEx` the user can still unlock with the phone's PIN, pattern or password (Android 11+).

### `isKeyHardwareBacked`

```js
window.WebToApk.isKeyHardwareBacked(alias: String): String
```

True when [alias] lives in the TEE or a secure element rather than in software. Worth checking before promising a user that destroying a key is irreversible.

**Example**

Tells you whether a key lives inside the phone's secure hardware (TEE or StrongBox chip) rather than in software. Most pages use `AppMintKeystore.isHardwareBacked(alias)`, which returns a real boolean.

**Returns:** the STRING `"true"` or `"false"`, at once. `"false"` also when the key does not exist. **Needs:** **Secure Keys (encryption)** turned on when you build.

```js
function describeVault() {
  if (!window.WebToApk || typeof window.WebToApk.isKeyHardwareBacked !== 'function') return;
  var hw = window.WebToApk.isKeyHardwareBacked('vault') === 'true';
  document.getElementById('vault-info').textContent = hw
    ? 'Your key is locked in this phone\'s secure hardware.'
    : 'Your key is protected by software on this phone.';
}
```

Refuse to keep a high-value secret on a phone that can only hold the key in software:

```js
function setUpWalletKey() {
  var W = window.WebToApk;
  if (!W || typeof W.isKeyHardwareBacked !== 'function') return false;
  var r = JSON.parse(W.generateKey('wallet', JSON.stringify({ requireAuth: true })));
  if (!r.ok) return false;
  if (W.isKeyHardwareBacked('wallet') !== 'true') {
    W.deleteKey('wallet');
    alert('This phone cannot keep the wallet key in secure hardware, so the wallet is off here.');
    return false;
  }
  return true;
}
```

Send your server a small device-security summary with sign-in, so it can decide what to allow:

```js
function deviceSecurity() {
  var W = window.WebToApk;
  var inApp = !!W && typeof W.isKeyHardwareBacked === 'function';
  return {
    secureKeys: inApp && W.isKeystoreEnabled() === 'true',
    hardwareKey: inApp && W.isKeyHardwareBacked('vault') === 'true'
  };
}

fetch('/api/sign-in', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: email, password: password, device: deviceSecurity() })
});
```

**Notes:** Say "hardware" in your UI only when this is `"true"`.

### `isKeystoreEnabled`

```js
window.WebToApk.isKeystoreEnabled(): String
```

Whether the creator enabled the keystore bridge for this app.

**Example**

Tells you whether the creator turned on the Keystore bridge for this app. Most pages use `AppMintKeystore.isAvailable()`, which returns a real boolean.

**Returns:** the STRING `"true"` or `"false"`, at once. **Needs:** nothing to call it; it answers `"true"` only when **Secure Keys (encryption)** was turned on when you built.

```js
function keystoreReady() {
  return !!window.WebToApk &&
    typeof window.WebToApk.isKeystoreEnabled === 'function' &&
    window.WebToApk.isKeystoreEnabled() === 'true';
}

document.getElementById('vault-button').hidden = !keystoreReady();
```

Offer "Remember me" only when the token can be stored encrypted:

```js
var canSeal = !!window.WebToApk &&
  typeof window.WebToApk.isKeystoreEnabled === 'function' &&
  window.WebToApk.isKeystoreEnabled() === 'true';

var remember = document.getElementById('remember-me');
remember.parentElement.hidden = !canSeal;

function afterSignIn(token) {
  if (!canSeal || !remember.checked) return;      // nothing stored in plain text
  if (window.WebToApk.hasKey('session') !== 'true') window.WebToApk.generateKey('session', '{}');
  var r = JSON.parse(window.WebToApk.encryptWithKey('session', btoa(token)));
  if (r.ok) localStorage.setItem('token', r.base64);
}
```

Tell apart "opened in a browser" and "app built without Secure Keys" while you test:

```js
function keystoreStatus() {
  if (!window.WebToApk) return 'browser: no app bridge';
  if (typeof window.WebToApk.isKeystoreEnabled !== 'function') return 'app without the keystore bridge';
  return window.WebToApk.isKeystoreEnabled() === 'true'
    ? 'Secure Keys on'
    : 'Secure Keys off in this build';
}

console.log('Keystore:', keystoreStatus());
```

**Notes:** When it is `"false"`, every other key method answers `"false"`, `"[]"` or `{ok:false, error:"disabled"}`. Hide the feature instead of offering a button that cannot work.

### `listKeys`

```js
window.WebToApk.listKeys(): String
```

JSON array of every key alias this page has created.

**Example**

Lists the aliases of every Keystore key this app's pages created. Most pages use `AppMintKeystore.listKeys()`, which returns the parsed array.

**Returns:** a JSON array string, sorted by name, at once: `'["notes","vault"]'`. **Needs:** **Secure Keys (encryption)** turned on when you build (without it: `"[]"`).

```js
function showKeys() {
  if (!window.WebToApk || typeof window.WebToApk.listKeys !== 'function') return;
  var aliases = JSON.parse(window.WebToApk.listKeys());
  document.getElementById('keys').textContent =
    aliases.length ? aliases.join(', ') : 'No keys yet';
}
```

A "Security keys" settings screen that lists each key, where it lives, and a Delete button:

```js
function renderKeyManager() {
  if (!window.WebToApk || typeof window.WebToApk.listKeys !== 'function') return;
  var list = document.getElementById('key-list');
  list.innerHTML = '';
  JSON.parse(window.WebToApk.listKeys()).forEach(function (alias) {
    var hw = window.WebToApk.isKeyHardwareBacked(alias) === 'true';
    var li = document.createElement('li');
    li.textContent = alias + (hw ? ' (secure hardware) ' : ' (software) ');
    var del = document.createElement('button');
    del.textContent = 'Delete';
    del.onclick = function () {
      if (!confirm('Delete "' + alias + '"? Data sealed with it is lost for good.')) return;
      window.WebToApk.deleteKey(alias);
      renderKeyManager();
    };
    li.appendChild(del);
    list.appendChild(li);
  });
}
```

On sign-out, destroy every per-account key (named `user_...`) so nothing cached on the phone can be read again:

```js
function forgetAllAccounts() {
  if (!window.WebToApk || typeof window.WebToApk.listKeys !== 'function') return 0;
  var removed = 0;
  JSON.parse(window.WebToApk.listKeys()).forEach(function (alias) {
    if (alias.indexOf('user_') === 0 && window.WebToApk.deleteKey(alias) === 'true') removed++;
  });
  return removed;
}

document.getElementById('sign-out').addEventListener('click', function () {
  var n = forgetAllAccounts();
  console.log('Destroyed ' + n + ' account key(s)');
  signOut();
});
```

**Notes:** Also answers `"[]"` when it is called from a page that is not the app's own (another site loaded in the app).

