# 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:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.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.

**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.');
}
```

**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.

**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);
```

**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();
});
```

**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)';
}
```

**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);
  });
}
```

**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);
});
```

**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;
}
```

**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.');
}
```

**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;
}
```

**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', '{}'));
```

**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();
}
```

**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
  }
}
```

**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.';
}
```

**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();
```

**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';
}
```

**Notes:** Only keys made through the bridge are listed; the app's own internal keys are never shown. Also answers `"[]"` when it is called from a page that is not the app's own (another site loaded in the app).

