Crear APK gratis

API del puente de JavaScript

Biometría y keystore

Desbloqueo con huella y rostro, y claves respaldadas por hardware con las que la página puede cifrar pero que nunca puede leer.

AppMintKeystore.decrypt ayudante#

window.AppMintKeystore.decrypt(alias, base64)

Ejemplo

Descifra los datos creados por AppMintKeystore.encrypt con la misma clave.

Devuelve: un objeto, al instante (no una Promise): {ok:true, base64} (los datos en claro) o {ok:false, error, detail}. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Para un archivo escrito con WebToApkFS.writeSealed, léelo de nuevo con WebToApkFS.readSealed(path, alias); mismo formato de resultado.

Lee de vuelta un objeto JSON sellado, desbloqueando primero cuando la clave lo necesite:

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

Abre una lista de entradas de diario selladas, manteniendo visibles como «bloqueadas» las que fallan:

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

Notas: Después de deleteKey los datos se pierden para siempre; ese es el objetivo. Un contenido que fue modificado falla la comprobación GCM. Otros errores: bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.deleteKey ayudante#

window.AppMintKeystore.deleteKey(alias)

Ejemplo

Destruye una clave segura de forma permanente. Todo lo que se cifró con ella no podrá volver a leerse nunca, por nadie.

Devuelve: true (borrada) o false (no existe esa clave, o la función está desactivada), al instante. Requiere: Claves seguras (cifrado) activadas al compilar.

Sobrescribir un archivo no lo borra de la memoria flash. Cífralo y luego destruye la clave: eso sí es un borrado real:

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

«Borrar todo» en los ajustes: destruye todas las claves que creó esta app y luego borra el almacenamiento de la página:

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

Autodestruye la bóveda tras demasiados PIN de la app incorrectos:

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

Notas: no hay deshacer ni recuperación. Si isHardwareBacked(alias) era false, la clave vivía en software: dilo antes de prometer al usuario un borrado irreversible.

AppMintKeystore.encrypt ayudante#

window.AppMintKeystore.encrypt(alias, base64)

Ejemplo

Cifra datos en base64 con una clave segura. Guarda el resultado donde quieras: es inútil sin la clave.

Devuelve: un objeto, de inmediato (no es una Promise): {ok:true, base64} (IV + texto cifrado + etiqueta) o {ok:false, error, detail}. Requiere: Claves seguras (cifrado) activadas al compilar y una clave de AppMintKeystore.generateKey.

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 se puede crear en authenticateBiometricEx (mira ese ejemplo).

Para escribir un ARCHIVO cifrado de cualquier tamaño, usa WebToApkFS.writeSealed(path, base64, alias): cifra por fragmentos con esta misma clave.

Sella un pequeño objeto JSON (una nota, un token, ajustes) y guárdalo en localStorage:

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

Crea la clave la primera vez que se necesite y luego cifra:

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

Notas: máximo 4 MB por llamada (chunk too large). Errores: no-such-key, auth-required, key-invalidated (el bloqueo de pantalla cambió: la clave se perdió), bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.generateKey ayudante#

window.AppMintKeystore.generateKey(alias, options)

Ejemplo

Crea (o reemplaza) una clave AES-256-GCM en el Android Keystore. La clave nunca sale del hardware seguro; tu página solo usa su nombre.

Devuelve: un objeto, al instante (no una Promise): {ok:true, alias, hardwareBacked} o {ok:false, error, detail}. Requiere: Claves seguras (cifrado) activadas al compilar (consulta AppMintKeystore.isAvailable).

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

Una clave simple, sin necesidad de desbloqueo:

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

Exige el chip seguro independiente (StrongBox). Falla en lugar de darte una clave más débil:

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

Notas: Alias: letras, dígitos, _ - ., hasta 64 caracteres (error:"bad alias" en caso contrario). Usar un alias existente reemplaza la clave, y los datos sellados con la anterior se pierden. Otros errores: disabled, bad options json, foreign_origin, unsupported (sin puente).

AppMintKeystore.hasKey ayudante#

window.AppMintKeystore.hasKey(alias)

Ejemplo

Te indica si existe una clave segura con este alias.

Devuelve: true / false, al instante. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Asegúrate de que exista una clave antes del primer guardado, creándola solo una vez:

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

Limpia los datos sellados cuya clave ya no existe (se borró, o los datos de la app se restauraron sin ella):

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

Notas: Responde false cuando las Claves seguras están desactivadas, así que comprueba isAvailable() primero si necesitas distinguir «aún no hay clave» de «función no incluida en esta app».

AppMintKeystore.isAvailable ayudante#

window.AppMintKeystore.isAvailable()

Ejemplo

Te indica si en esta app se pueden usar claves seguras respaldadas por hardware.

Devuelve: true / false, de inmediato (no es una Promise). Requiere: Claves seguras (cifrado) activadas al compilar. El objeto window.AppMintKeystore se instala (antes de que se ejecute tu primer script) siempre que Claves seguras o Acceso nativo a carpetas (SAF) estén activados; isAvailable() responde por las Claves seguras.

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

Oculta todos los controles de la bóveda segura cuando la función no está en esta app:

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

Distingue "función desactivada" de "todavía no hay clave" al decidir qué pantalla mostrar:

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

Notas: El helper se añade después de que la página haya cargado, así que no lo compruebes en un script que se ejecute mientras la página aún se analiza; comprueba en un controlador de clic o después de load. En un navegador no existe window.AppMintKeystore; la comprobación anterior devuelve false sin lanzar errores.

AppMintKeystore.isHardwareBacked ayudante#

window.AppMintKeystore.isHardwareBacked(alias)

Ejemplo

Te indica si una clave vive en el hardware seguro del teléfono (TEE o StrongBox) en lugar de en software.

Devuelve: true / false, al instante. false cuando la clave no existe. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Guarda un secreto de gran valor (una semilla de cartera, un código de recuperación) solo cuando su clave esté en hardware seguro:

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

Lista dónde vive cada clave, para un informe de soporte:

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

Notas: generateKey también devuelve hardwareBacked justo después de crear la clave.

AppMintKeystore.listKeys ayudante#

window.AppMintKeystore.listKeys()

Ejemplo

Lista los alias de cada clave segura que crearon tus páginas.

Devuelve: un array de cadenas, ordenado, al instante, p. ej. ['notes', 'vault']. [] cuando no hay ninguna o la función está desactivada. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Destruye todas las claves con "Borrar todos mis datos", lo que hace ilegible todo lo sellado con ellas:

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

Mantén una clave por cada usuario con sesión iniciada y elimina las claves de los usuarios que cerraron sesión:

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

Notas: solo se listan las claves creadas mediante el puente; las claves internas de la propia app nunca se muestran.

authenticateBiometric puente#

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

WebToApk.isBiometricAvailable() -> "available" | "no_hardware" | "not_enrolled" | "unavailable" WebToApk.authenticateBiometric(id, title, subtitle) El resultado se entrega a la misma tabla de callbacks __webToApkAuth que usa el puente de Google Sign-In: 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');

Descrito por su grupo, Puente de huella / biometría, y no por sí mismo.

Ejemplo

Muestra el aviso de huella del sistema con un título y un subtítulo. La forma simple de authenticateBiometricEx (usa esa para la opción de PIN, una etiqueta de Cancelar personalizada o biometría "fuerte").

Devuelve: nada. La respuesta llega después: el shell llama a window.__webToApkAuth[callbackId](json) con '{"ok":true}' o '{"ok":false,"error":"<code>"}', y luego elimina esa entrada. Requiere: Huella marcada al compilar (sin ella: error:"permission_missing").

Registra PRIMERO el callback y luego llama:

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

Confirma primero una acción de riesgo con la huella (un título vacío muestra el nombre de la app):

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

Bloquea la app de nuevo cuando vuelva tras más de un minuto en segundo plano:

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

Notas: Un título vacío usa el nombre de la app. Un dedo equivocado NO termina el aviso; el callback se activa solo con éxito, cancelación o un error real. Los códigos de error son los mismos que en authenticateBiometricEx. Esto demuestra que el dueño del teléfono está presente; no demuestra quién es ante un servidor.

authenticateBiometricEx puente#

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

La forma completa. [optionsJson] acepta: title, subtitle, description, negativeButtonText, allowDeviceCredential (alternativa con PIN/patrón/contraseña), strong (exige BIOMETRIC_STRONG: úsalo para cualquier cosa relacionada con pagos). Los errores son códigos distintos, no un solo booleano. Antes de cualquier aviso: no_hardware | none_enrolled | unavailable (no se pudo ejecutar la comprobación de estado; consulta isBiometricAvailable). Del aviso del sistema de Android 9+: user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error, y para un código que Android añada más adelante, su propio texto de mensaje (o `error_<code>`). permission_missing cuando la compilación no tiene el permiso Fingerprint. En Android 7-8 (API 24-27, solo huella) un error es el texto del propio mensaje del teléfono tal como lo escribió el fabricante, no un código fijo.

Ejemplo

Muestra el aviso de huella / rostro del sistema con todas las opciones: PIN o patrón como segunda vía de acceso, una etiqueta de Cancelar personalizada y solo biometría "fuerte".

Devuelve: nada. La respuesta llega después: el shell llama a window.__webToApkAuth[callbackId](json) con '{"ok":true}' o '{"ok":false,"error":"<code>"}', y luego elimina esa entrada. Requiere: Huella marcada al compilar (sin ella: error:"permission_missing").

Un pequeño envoltorio de Promise que puedes reutilizar:

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

Desbloquea la app: huella, rostro o el PIN/patrón del teléfono:

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

Confirma un pago, solo con biometría fuerte y tu propia etiqueta de Cancelar:

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

Desbloquea una clave del Keystore creada con requireAuth y reintenta:

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

Notas: códigos de error: user_cancel, none_enrolled, no_hardware, hw_unavailable, lockout (espera y reintenta), lockout_permanent (el usuario debe desbloquear primero el teléfono con el PIN), no_device_credential, timeout, permission_missing, unavailable, y rara vez no_space, unable_to_process, vendor_error. En Android 7-8 el shell muestra su propio diálogo y un error es el texto de mensaje del sistema. allowDeviceCredential y negativeButtonText no se combinan: con la opción de PIN, el sistema dibuja su propio botón. Un dedo equivocado no termina el aviso.

decryptWithKey puente#

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

Revierte [encryptWithKey]. Falla con `error:"no-such-key"` una vez que la clave se ha eliminado; el contenido entonces es irrecuperable por diseño. @return JSON `{ok, base64, error}`.

Ejemplo

Descifra datos creados por encryptWithKey con la misma clave del Keystore. La mayoría de las páginas usan AppMintKeystore.decrypt(alias, base64), que devuelve el objeto ya interpretado.

Devuelve: una cadena JSON, al instante: {"ok":true,"base64":"<plain data>"} o {"ok":false,"error":"...","detail":"..."}. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Para una clave creada con requireAuth:true: ante auth-required, muestra el aviso de desbloqueo y luego descifra de nuevo:

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

Muestra una foto cifrada: descífrala a bytes y luego muéstrala mediante una URL de blob:

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

Notas: Una vez eliminada la clave (deleteKey), los datos no podrán descifrarse nunca más, por diseño. Un contenido modificado o dañado falla (la etiqueta GCM no coincide); entonces el error es el mensaje de la plataforma, no uno de los códigos anteriores. Otros códigos: bad base64, disabled, foreign_origin.

deleteKey puente#

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

Destruye [alias] de forma permanente. Todo lo que se cifró con él queda imposible de descifrar, incluso para esta app y para cualquiera que tenga el dispositivo.

Ejemplo

Destruye una clave del Keystore para siempre. Todo lo cifrado con ella no podrá volver a leerse jamás: ni por ti, ni por el usuario, ni por quien tenga el teléfono. La mayoría de las páginas usan AppMintKeystore.deleteKey(alias), que devuelve un booleano real.

Devuelve: la CADENA "true" (eliminada) o "false" (no existe la clave, Claves seguras desactivadas o no es la página de la propia app), de inmediato. Requiere: Claves seguras (cifrado) activadas al compilar.

Este es el único «borrado seguro» real en un teléfono: sobrescribir un archivo no lo borra en la memoria flash, destruir la clave sí.

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

Rota una clave: abre los datos con la clave antigua, séllalos con una nueva y luego destruye la clave antigua:

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
}

«Olvidarme en este teléfono» al cerrar sesión: destruye la clave de la cuenta y luego elimina sus datos en caché:

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

Notas: Comprueba isKeyHardwareBacked antes de prometer a un usuario que la eliminación es irreversible: una clave solo en software es más débil.

encryptWithKey puente#

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

Cifra el base64 [base64Plaintext] con [alias]. Cada llamada genera su propio IV, así que un archivo grande se cifra por fragmentos; los fragmentos son independientes y pueden descifrarse en cualquier orden. @return JSON `{ok, base64, error}` donde `base64` es `IV || ciphertext || tag`.

Ejemplo

Cifra datos en base64 con una clave del Keystore. La mayoría de las páginas usan AppMintKeystore.encrypt(alias, base64), que devuelve el objeto ya interpretado.

Devuelve: una cadena JSON, al instante: {"ok":true,"base64":"<IV + ciphertext + tag>"} o {"ok":false,"error":"...","detail":"..."}. Requiere: Claves seguras (cifrado) activadas al compilar, y una clave creada con generateKey.

La entrada es base64. Para texto, conviértelo primero a base64 UTF-8:

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

Sella un objeto entero (una sesión, algunos ajustes) como JSON antes de guardarlo:

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

Cifra una foto que elige el usuario (datos binarios, hasta 4 MB por llamada):

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

Notas: Cada llamada usa un IV nuevo, así que el mismo texto da un resultado distinto cada vez. Máximo 4 MB por llamada (error:"chunk too large ..."); para archivos más grandes cifra por fragmentos o usa WebToApkFS.writeSealed. Códigos de error: no-such-key, auth-required, key-invalidated (el usuario cambió el bloqueo de pantalla; la clave se perdió para siempre), bad base64, disabled, foreign_origin.

generateKey puente#

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

Crea (o reemplaza) una clave AES-256-GCM llamada [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` vincula la clave al bloqueo del dispositivo; `strongBox` falla por completo en dispositivos sin elemento seguro en lugar de entregar una clave más débil de la pedida. @return JSON `{ok, alias, hardwareBacked, error}`.

Ejemplo

Crea (o reemplaza) una clave AES-256-GCM en el Android Keystore. La página recibe un nombre para la clave, nunca la clave en sí. La mayoría de las páginas usan AppMintKeystore.generateKey(alias, options), que hace por ti el trabajo con JSON.

Devuelve: una cadena JSON, al instante: {"ok":true,"alias":"vault","hardwareBacked":true} o {"ok":false,"error":"...","detail":"..."}. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Una clave simple sin regla de desbloqueo (las opciones pueden ser '{}'):

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

Crea la clave una sola vez, la primera vez que se necesite: llamar a generateKey de nuevo la reemplazaría y dejaría huérfano todo lo ya sellado:

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

Exige el chip seguro independiente para una clave de cartera e informa claramente al usuario cuando el teléfono no lo tiene:

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

Notas: el alias solo puede usar letras, dígitos, _, -, . y como máximo 64 caracteres; de lo contrario, error:"bad alias". Llamarlo de nuevo con el mismo alias REEMPLAZA la clave, y los datos sellados con la clave antigua ya no se pueden leer. Códigos de error: disabled (Claves seguras no activadas), bad options json, strongbox-unavailable (pediste strongBox:true y el teléfono no tiene chip seguro, o es anterior a Android 9), foreign_origin (llamado desde una página que no es la propia de la app).

hasKey puente#

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

Verdadero cuando existe una clave llamada [alias] en el keystore de esta app.

Ejemplo

Te indica si existe una clave del Keystore con este alias. La mayoría de las páginas usan AppMintKeystore.hasKey(alias), que devuelve un booleano real.

Devuelve: la CADENA "true" o "false", al instante. Requiere: Claves seguras (cifrado) activadas al compilar (sin ellas la respuesta siempre es "false").

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

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

Elige la pantalla de inicio: desbloquear una bóveda existente o crear una, y limpiar los datos sellados cuya clave ya no existe:

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

Una clave por cada cuenta con sesión iniciada, creada la primera vez que esa cuenta inicia sesión en este teléfono:

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

Notas: la respuesta es una cadena, así que if (WebToApk.hasKey('vault')) siempre es verdadero: compara con === 'true'. Solo ves las claves que esta app creó mediante el puente; las claves propias del shell están ocultas.

isBiometricAvailable puente#

window.WebToApk.isBiometricAvailable(): String

"available" | "no_hardware" | "not_enrolled" | "unavailable". Android 10+ cuenta rostro, iris y huella; Android 6-9 solo el sensor de huellas. "unavailable" = el sensor no se puede usar temporalmente, o la comprobación no pudo ejecutarse (p. ej., la compilación no activó el permiso de Huella). Consulta [BiometricStatus].

Ejemplo

Te dice si el teléfono puede hacer un desbloqueo biométrico (rostro, iris o huella) ahora mismo. Compruébalo antes de mostrar un botón de desbloqueo.

Devuelve: una cadena, al instante: "available", "no_hardware" (sin sensor), "not_enrolled" (hay sensor, pero no hay nada registrado, o no hay bloqueo de pantalla) o "unavailable" (el sensor no se puede usar temporalmente, o la comprobación no pudo ejecutarse; por ejemplo, la compilación no marcó Huella). Requiere: Huella marcada al compilar: sin ella Android se niega a responder y el resultado es "unavailable".

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

Un interruptor de ajustes "Desbloquear con huella" que aparece atenuado con el motivo cuando no puede funcionar:

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

Vuelve a comprobar cuando el usuario regrese de los ajustes del teléfono, por si acaba de añadir una huella:

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

Notas: en Android 10 y posteriores la comprobación cuenta rostro, iris y huella, así que un teléfono solo con desbloqueo facial responde "available". En Android 6-9 solo mira el sensor de huellas, así que un teléfono solo con rostro allí responde "no_hardware"; con allowDeviceCredential:true en authenticateBiometricEx el usuario aún puede desbloquear con el PIN, el patrón o la contraseña del teléfono (Android 11+).

isKeyHardwareBacked puente#

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

Verdadero cuando [alias] vive en el TEE o en un elemento seguro y no en software. Conviene comprobarlo antes de prometer al usuario que destruir una clave es irreversible.

Ejemplo

Te indica si una clave vive dentro del hardware seguro del teléfono (TEE o chip StrongBox) en lugar de en software. La mayoría de las páginas usan AppMintKeystore.isHardwareBacked(alias), que devuelve un booleano real.

Devuelve: la CADENA "true" o "false", de inmediato. "false" también cuando la clave no existe. Requiere: Claves seguras (cifrado) activadas al compilar.

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

Rechaza guardar un secreto de gran valor en un teléfono que solo puede guardar la clave por software:

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

Envía a tu servidor un pequeño resumen de seguridad del dispositivo al iniciar sesión, para que pueda decidir qué permitir:

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

Notas: di "hardware" en tu interfaz solo cuando esto sea "true".

isKeystoreEnabled puente#

window.WebToApk.isKeystoreEnabled(): String

Si el creador activó el puente del keystore para esta app.

Ejemplo

Te dice si el creador activó el puente del Keystore para esta app. La mayoría de las páginas usan AppMintKeystore.isAvailable(), que devuelve un booleano real.

Devuelve: la CADENA "true" o "false", al instante. Requiere: nada para llamarlo; responde "true" solo cuando Claves seguras (cifrado) estaban activadas al compilar.

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

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

Ofrece "Recordarme" solo cuando el token pueda guardarse cifrado:

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

Distingue entre «abierto en un navegador» y «app compilada sin Claves seguras» mientras pruebas:

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

Notas: cuando es "false", todos los demás métodos de claves responden "false", "[]" o {ok:false, error:"disabled"}. Oculta la función en lugar de ofrecer un botón que no puede funcionar.

listKeys puente#

window.WebToApk.listKeys(): String

Array JSON con cada alias de clave que esta página ha creado.

Ejemplo

Lista los alias de cada clave del Keystore que crearon las páginas de esta app. La mayoría de las páginas usan AppMintKeystore.listKeys(), que devuelve el array ya interpretado.

Devuelve: una cadena de matriz JSON, ordenada por nombre, de inmediato: '["notes","vault"]'. Requiere: Claves seguras (cifrado) activadas al compilar (sin ello: "[]").

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

Una pantalla de ajustes «Claves de seguridad» que lista cada clave, dónde vive y un botón Eliminar:

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

Al cerrar sesión, destruye todas las claves de cada cuenta (llamadas user_...) para que nada guardado en caché en el teléfono pueda volver a leerse:

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

Notas: Solo se listan las claves creadas mediante el puente; las claves internas de la propia app nunca se muestran. También responde "[]" cuando se llama desde una página que no es la de la propia app (otro sitio cargado dentro de la app).

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.