Criar APK grátis

API da ponte JavaScript

Biometria e keystore

Desbloqueio por impressão digital e rosto, e chaves protegidas por hardware com as quais a página pode criptografar, mas nunca ler.

AppMintKeystore.decrypt helper#

window.AppMintKeystore.decrypt(alias, base64)

Exemplo

Descriptografa dados criados por AppMintKeystore.encrypt com a mesma chave.

Retorna: um objeto, imediatamente (não uma Promise): {ok:true, base64} (os dados em claro) ou {ok:false, error, detail}. Requer: Chaves seguras (criptografia) ativadas ao 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 um arquivo gravado com WebToApkFS.writeSealed, leia-o de volta com WebToApkFS.readSealed(path, alias), com o mesmo formato de resultado.

Leia de volta um objeto JSON selado, desbloqueando antes quando a chave exigir:

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

Abra uma lista de anotações de diário lacradas, mantendo visíveis como "bloqueadas" as que falharem:

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

Observações: Depois de deleteKey, os dados se perdem para sempre; esse é o objetivo. Um conteúdo que foi alterado falha na verificação GCM. Outros erros: bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.deleteKey helper#

window.AppMintKeystore.deleteKey(alias)

Exemplo

Destrói uma chave segura de forma permanente. Tudo o que foi criptografado com ela nunca mais poderá ser lido, por ninguém.

Retorna: true (excluída) ou false (chave inexistente, ou o recurso está desativado), imediatamente. Requer: Chaves seguras (criptografia) ativadas ao compilar.

Sobrescrever um arquivo não o apaga na memória flash. Criptografe-o e depois destrua a chave - isso é uma exclusão de verdade:

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

"Apagar tudo" nas configurações: destrua todas as chaves que este app criou e depois limpe o armazenamento da 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();
}

Autodestrua o cofre após PINs do app errados demais:

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

Observações: Não há desfazer nem recuperação. Se isHardwareBacked(alias) foi false, a chave vivia em software - diga isso antes de prometer ao usuário uma exclusão irreversível.

AppMintKeystore.encrypt helper#

window.AppMintKeystore.encrypt(alias, base64)

Exemplo

Criptografa dados em base64 com uma chave segura. Guarde o resultado em qualquer lugar - ele é inútil sem a chave.

Retorna: um objeto, na hora (não é uma Promise): {ok:true, base64} (IV + texto cifrado + tag) ou {ok:false, error, detail}. Requer: Chaves seguras (criptografia) ativadas ao compilar e uma chave criada com 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 pode ser criado com authenticateBiometricEx (veja esse exemplo).

Para gravar um ARQUIVO criptografado de qualquer tamanho, use WebToApkFS.writeSealed(path, base64, alias), que criptografa em partes com esta mesma chave.

Sele um pequeno objeto JSON (uma nota, um token, configurações) e guarde no 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);

Crie a chave na primeira vez que for necessária e depois criptografe:

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

Observações: Máx. 4 MB por chamada (chunk too large). Erros: no-such-key, auth-required, key-invalidated (o bloqueio de tela mudou - a chave se foi), bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.generateKey helper#

window.AppMintKeystore.generateKey(alias, options)

Exemplo

Cria (ou substitui) uma chave AES-256-GCM no Android Keystore. A chave nunca sai do hardware seguro; sua página usa apenas o nome dela.

Retorna: um objeto, imediatamente (não uma Promise): {ok:true, alias, hardwareBacked} ou {ok:false, error, detail}. Requer: Chaves seguras (criptografia) ativadas ao compilar (veja 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);
}

Uma chave simples, sem necessidade de desbloqueio:

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

Exija o chip seguro separado (StrongBox). Se não houver, falha em vez de lhe dar uma chave mais fraca:

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

Observações: Alias: letras, dígitos, _ - ., até 64 caracteres (error:"bad alias" caso contrário). Usar um alias existente substitui a chave, e os dados lacrados com a antiga se perdem. Outros erros: disabled, bad options json, foreign_origin, unsupported (sem ponte).

AppMintKeystore.hasKey helper#

window.AppMintKeystore.hasKey(alias)

Exemplo

Informa se existe uma chave segura com este alias.

Retorna: true / false, imediatamente. Requer: Chaves seguras (criptografia) ativadas ao compilar.

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

Garanta que uma chave exista antes do primeiro salvamento, criando-a uma única 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();

Limpe dados selados cuja chave sumiu (excluída, ou os dados do app foram restaurados sem ela):

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

Observações: Responde false quando as Chaves seguras estão desativadas, então verifique isAvailable() primeiro se precisar distinguir "ainda não há chave" de "recurso não incluído neste app".

AppMintKeystore.isAvailable helper#

window.AppMintKeystore.isAvailable()

Exemplo

Informa se chaves seguras protegidas por hardware podem ser usadas neste app.

Retorna: true / false, na hora (não é uma Promise). Requer: Chaves seguras (criptografia) ativadas ao compilar. O objeto window.AppMintKeystore é instalado (antes de o seu primeiro script rodar) sempre que Chaves seguras ou Acesso nativo a pastas (SAF) estiver ativado; isAvailable() responde pelas Chaves 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();
});

Oculte todos os controles do cofre seguro quando o recurso não estiver neste app:

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

Diferencie "recurso desativado" de "ainda sem chave" ao decidir qual tela 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();
});

Observações: O auxiliar é adicionado depois que a página carrega, então não o teste em um script que roda enquanto a página ainda está sendo lida: teste em um manipulador de clique ou depois de load. Em um navegador não existe window.AppMintKeystore; a verificação acima retorna false sem gerar erro.

AppMintKeystore.isHardwareBacked helper#

window.AppMintKeystore.isHardwareBacked(alias)

Exemplo

Informa se uma chave fica no hardware seguro do celular (TEE ou StrongBox) em vez de em software.

Retorna: true / false, imediatamente. false quando a chave não existe. Requer: Chaves seguras (criptografia) ativadas ao 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)';
}

Só guarde um segredo de alto valor (uma seed de carteira, um código de recuperação) quando a chave dele estiver em 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.');
}

Liste onde cada chave está guardada, para um relatório de suporte:

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

Observações: generateKey também retorna hardwareBacked logo após criar a chave.

AppMintKeystore.listKeys helper#

window.AppMintKeystore.listKeys()

Exemplo

Lista os aliases de toda chave segura que suas páginas criaram.

Retorna: um array de strings, ordenado, imediatamente, por exemplo ['notes', 'vault']. [] quando não há nenhuma ou o recurso está desativado. Requer: Chaves seguras (criptografia) ativadas ao 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);
  });
}

Destrua todas as chaves em "Apagar todos os meus dados", o que torna ilegível tudo que foi selado com elas:

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

Mantenha uma chave por usuário conectado e remova as chaves de quem saiu da conta:

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

Observações: Somente as chaves criadas pela ponte são listadas; as chaves internas do próprio app nunca são mostradas.

authenticateBiometric ponte#

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

WebToApk.isBiometricAvailable() -> "available" | "no_hardware" | "not_enrolled" | "unavailable" WebToApk.authenticateBiometric(id, title, subtitle) O resultado é entregue à mesma tabela de callbacks __webToApkAuth que a ponte do Login com Google usa: 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 pelo seu grupo, Ponte de impressão digital / biometria, e não isoladamente.

Exemplo

Mostra o pedido de impressão digital do sistema com título e subtítulo. A forma simples de authenticateBiometricEx (use aquela para a opção de PIN, um rótulo de Cancelar personalizado ou biometria "forte").

Retorna: nada. A resposta chega depois: o shell chama window.__webToApkAuth[callbackId](json) com '{"ok":true}' ou '{"ok":false,"error":"<code>"}' e depois remove essa entrada. Requer: Impressão digital marcada ao compilar (sem ela: error:"permission_missing").

Registre o callback PRIMEIRO e depois chame:

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

Confirme antes uma ação de risco com a impressão digital (um título vazio mostra o nome do 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);
});

Bloqueie o app de novo quando ele voltar após mais de um minuto em 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
});

Observações: Um título vazio usa o nome do app. Um dedo errado NÃO encerra o prompt: o callback só dispara em caso de sucesso, cancelamento ou erro real. Os códigos de erro são os mesmos de authenticateBiometricEx. Isso prova que o dono do celular está presente; não prova quem ele é para um servidor.

authenticateBiometricEx ponte#

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

A forma completa. [optionsJson] aceita: title, subtitle, description, negativeButtonText, allowDeviceCredential (alternativa com PIN/padrão/senha), strong (exige BIOMETRIC_STRONG: use em tudo que envolva pagamento). Os erros são códigos distintos, não um único booleano. Antes de qualquer prompt: no_hardware | none_enrolled | unavailable (a verificação de status não pôde rodar; veja isBiometricAvailable). Do prompt do sistema do Android 9+: user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error e, para um código que o Android adicionar depois, o próprio texto de mensagem dele (ou `error_<code>`). permission_missing quando o build não tem a permissão Fingerprint. No Android 7-8 (API 24-27, somente impressão digital), o erro é o texto de mensagem do próprio celular, como o fabricante escreveu, e não um código fixo.

Exemplo

Mostra o pedido de impressão digital / rosto do sistema com todas as opções: PIN ou padrão como segunda forma de entrar, um rótulo de Cancelar personalizado e somente biometria "forte".

Retorna: nada. A resposta chega depois: o shell chama window.__webToApkAuth[callbackId](json) com '{"ok":true}' ou '{"ok":false,"error":"<code>"}' e depois remove essa entrada. Requer: Impressão digital marcada ao compilar (sem ela: error:"permission_missing").

Um pequeno wrapper de Promise que você pode 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));
  });
}

Desbloqueie o app - impressão digital, rosto, ou o PIN/padrão do celular:

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

Confirme um pagamento: somente biometria forte, com seu próprio rótulo 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);
}

Desbloqueie uma chave do Keystore criada com requireAuth e tente de novo:

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

Observações: Códigos de erro: user_cancel, none_enrolled, no_hardware, hw_unavailable, lockout (aguarde e tente de novo), lockout_permanent (o usuário precisa desbloquear o celular com o PIN antes), no_device_credential, timeout, permission_missing, unavailable, e raramente no_space, unable_to_process, vendor_error. No Android 7-8, o shell mostra o próprio diálogo e o erro é o texto de mensagem do sistema. allowDeviceCredential e negativeButtonText não combinam: com a opção de PIN, o sistema desenha o próprio botão. Um dedo errado não encerra o pedido.

decryptWithKey ponte#

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

Reverte [encryptWithKey]. Falha com `error:"no-such-key"` depois que a chave foi excluída: o conteúdo fica então irrecuperável, por projeto. @return JSON `{ok, base64, error}`.

Exemplo

Descriptografa dados feitos por encryptWithKey com a mesma chave do Keystore. A maioria das páginas usa AppMintKeystore.decrypt(alias, base64), que retorna o objeto já interpretado.

Retorna: uma string JSON, imediatamente: {"ok":true,"base64":"<plain data>"} ou {"ok":false,"error":"...","detail":"..."}. Requer: Chaves seguras (criptografia) ativadas ao 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 uma chave criada com requireAuth:true: em auth-required, mostre o pedido de desbloqueio e depois descriptografe de novo:

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

Mostre uma foto criptografada: descriptografe para bytes e depois exiba por uma 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;
}

Observações: Depois que a chave é excluída (deleteKey), os dados nunca mais podem ser descriptografados, por projeto. Um conteúdo alterado ou danificado falha (a tag GCM não confere); o error é então a mensagem da plataforma, não um dos códigos acima. Outros códigos: bad base64, disabled, foreign_origin.

deleteKey ponte#

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

Destrói [alias] de forma permanente. Tudo o que foi criptografado com ele deixa de poder ser descriptografado, inclusive por este app e por quem estiver com o aparelho.

Exemplo

Destrói uma chave do Keystore de forma definitiva. Tudo que foi criptografado com ela nunca mais poderá ser lido - por você, pelo usuário ou por quem estiver com o celular. A maioria das páginas usa AppMintKeystore.deleteKey(alias), que retorna um boolean de verdade.

Retorna: a STRING "true" (excluída) ou "false" (chave inexistente, Chaves seguras desativadas ou página que não é do próprio app), na hora. Requer: Chaves seguras (criptografia) ativadas ao compilar.

Esta é a única "exclusão segura" de verdade em um celular: sobrescrever um arquivo não o apaga na memória flash; destruir a chave, sim.

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

Faça a rotação de uma chave: abra os dados com a chave antiga, sele-os com uma nova e depois destrua a antiga:

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
}

"Esquecer-me neste celular" ao sair da conta: destrua a chave da conta e depois descarte os dados em cache dela:

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

Observações: Verifique isKeyHardwareBacked antes de prometer ao usuário que a exclusão é irreversível: uma chave somente em software é mais fraca.

encryptWithKey ponte#

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

Criptografa o base64 [base64Plaintext] com [alias]. Cada chamada gera seu próprio IV, então um arquivo grande é criptografado em partes: as partes são independentes e podem ser descriptografadas em qualquer ordem. @return JSON `{ok, base64, error}` em que `base64` é `IV || ciphertext || tag`.

Exemplo

Criptografa dados em base64 com uma chave do Keystore. A maioria das páginas usa AppMintKeystore.encrypt(alias, base64), que retorna o objeto já interpretado.

Retorna: uma string JSON, imediatamente: {"ok":true,"base64":"<IV + ciphertext + tag>"} ou {"ok":false,"error":"...","detail":"..."}. Requer: Chaves seguras (criptografia) ativadas ao compilar, e uma chave criada com generateKey.

A entrada é base64. Para texto, converta-o primeiro para base64 em 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;
}

Sele um objeto inteiro (uma sessão, algumas configurações) como JSON antes de guardá-lo:

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

Criptografe uma foto escolhida pelo usuário (dados binários, até 4 MB por chamada):

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

Observações: Cada chamada usa um IV novo, então o mesmo texto dá um resultado diferente a cada vez. No máximo 4 MB por chamada (error:"chunk too large ..."); para arquivos maiores, criptografe em partes ou use WebToApkFS.writeSealed. Códigos de erro: no-such-key, auth-required, key-invalidated (o usuário mudou o bloqueio de tela: a chave se perdeu para sempre), bad base64, disabled, foreign_origin.

generateKey ponte#

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

Cria (ou substitui) uma chave AES-256-GCM chamada [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` vincula a chave ao bloqueio do dispositivo; `strongBox` falha de vez em dispositivos sem elemento seguro em vez de entregar uma chave mais fraca do que a pedida. @return JSON `{ok, alias, hardwareBacked, error}`.

Exemplo

Cria (ou substitui) uma chave AES-256-GCM no Android Keystore. A página recebe um nome para a chave, nunca a chave em si. A maioria das páginas usa AppMintKeystore.generateKey(alias, options), que faz o trabalho de JSON para você.

Retorna: uma string JSON, imediatamente: {"ok":true,"alias":"vault","hardwareBacked":true} ou {"ok":false,"error":"...","detail":"..."}. Requer: Chaves seguras (criptografia) ativadas ao 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;
}

Uma chave simples, sem regra de desbloqueio (as opções podem ser '{}'):

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

Crie a chave uma única vez, na primeira vez que for necessária - chamar generateKey de novo a substituiria e deixaria órfão tudo que já foi selado:

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

Exija o chip seguro separado para uma chave de carteira e diga claramente ao usuário quando o celular não tiver um:

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

Observações: O alias só pode usar letras, dígitos, _, -, . e no máximo 64 caracteres, caso contrário error:"bad alias". Chamar de novo com o mesmo alias SUBSTITUI a chave, e os dados selados com a chave antiga não podem mais ser lidos. Códigos de erro: disabled (Chaves seguras não ativadas), bad options json, strongbox-unavailable (você pediu strongBox:true e o celular não tem chip seguro, ou é anterior ao Android 9), foreign_origin (chamado de uma página que não é a do próprio app).

hasKey ponte#

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

Verdadeiro quando uma chave chamada [alias] existe no keystore deste app.

Exemplo

Informa se existe uma chave do Keystore com este alias. A maioria das páginas usa AppMintKeystore.hasKey(alias), que retorna um booleano de verdade.

Retorna: a STRING "true" ou "false", imediatamente. Requer: Chaves seguras (criptografia) ativadas ao compilar (sem isso a resposta é sempre "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();
}

Escolha a tela inicial: desbloquear um cofre existente ou configurar um - e limpe dados selados cuja chave sumiu:

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

Uma chave por conta conectada, criada na primeira vez que essa conta entra neste celular:

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

Observações: A resposta é uma string, então if (WebToApk.hasKey('vault')) é sempre verdadeiro - compare com === 'true'. Você só vê as chaves que este app criou pela ponte; as chaves do próprio shell ficam ocultas.

isBiometricAvailable ponte#

window.WebToApk.isBiometricAvailable(): String

"available" | "no_hardware" | "not_enrolled" | "unavailable". O Android 10+ considera rosto, íris e impressão digital; o Android 6-9 apenas o sensor de impressão digital. "unavailable" = o sensor está temporariamente inutilizável, ou a verificação não pôde ser executada (por exemplo, o build não ativou a permissão de Impressão digital). Veja [BiometricStatus].

Exemplo

Informa se o celular consegue fazer um desbloqueio biométrico (rosto, íris ou impressão digital) agora. Verifique antes de mostrar um botão de desbloqueio.

Retorna: uma string, imediatamente: "available", "no_hardware" (sem sensor), "not_enrolled" (há sensor, mas nada cadastrado, ou sem bloqueio de tela definido) ou "unavailable" (o sensor está temporariamente inutilizável, ou a verificação não pôde ser executada - por exemplo, o build não marcou Impressão digital). Requer: Impressão digital marcada ao compilar - sem ela o Android se recusa a responder e o resultado é "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
  }
}

Um interruptor de configurações "Desbloquear com impressão digital" que fica desativado, com o motivo, quando não pode 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] || '');
}

Verifique de novo quando o usuário voltar das configurações do celular, caso ele tenha acabado de cadastrar uma impressão digital:

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

Observações: No Android 10 e posterior, a verificação considera rosto, íris e impressão digital, então um celular só com desbloqueio facial responde "available". No Android 6-9, ela olha apenas o sensor de impressão digital, então um celular só com rosto responde "no_hardware"; com allowDeviceCredential:true em authenticateBiometricEx, o usuário ainda pode desbloquear com o PIN, padrão ou senha do celular (Android 11+).

isKeyHardwareBacked ponte#

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

Verdadeiro quando [alias] vive no TEE ou em um elemento seguro, e não em software. Vale verificar antes de prometer ao usuário que destruir uma chave é irreversível.

Exemplo

Informa se uma chave fica dentro do hardware seguro do celular (TEE ou chip StrongBox) em vez de em software. A maioria das páginas usa AppMintKeystore.isHardwareBacked(alias), que retorna um booleano de verdade.

Retorna: a STRING "true" ou "false", na hora. "false" também quando a chave não existe. Requer: Chaves seguras (criptografia) ativadas ao 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.';
}

Recuse guardar um segredo de alto valor em um celular que só consegue manter a chave em 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;
}

Envie ao seu servidor um pequeno resumo de segurança do dispositivo junto com o login, para que ele decida o que 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() })
});

Observações: Diga "hardware" na sua interface somente quando isto for "true".

isKeystoreEnabled ponte#

window.WebToApk.isKeystoreEnabled(): String

Se o criador ativou a ponte do keystore para este app.

Exemplo

Informa se o criador ativou a ponte do Keystore para este app. A maioria das páginas usa AppMintKeystore.isAvailable(), que retorna um boolean de verdade.

Retorna: a STRING "true" ou "false", imediatamente. Requer: nada para chamá-lo; ele responde "true" somente quando as Chaves seguras (criptografia) foram ativadas ao compilar.

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

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

Ofereça "Lembrar de mim" somente quando o token puder ser guardado criptografado:

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

Diferencie "aberto em um navegador" de "app compilado sem Chaves seguras" durante seus testes:

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

Observações: Quando for "false", todos os outros métodos de chave respondem "false", "[]" ou {ok:false, error:"disabled"}. Oculte o recurso em vez de oferecer um botão que não pode funcionar.

listKeys ponte#

window.WebToApk.listKeys(): String

Array JSON com todos os aliases de chave que esta página criou.

Exemplo

Lista os aliases de toda chave do Keystore que as páginas deste app criaram. A maioria das páginas usa AppMintKeystore.listKeys(), que retorna o array já interpretado.

Retorna: uma string de array JSON, ordenada por nome, na hora: '["notes","vault"]'. Requer: Chaves seguras (criptografia) ativadas ao compilar (sem isso: "[]").

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

Uma tela de configurações "Chaves de segurança" que lista cada chave, onde ela está guardada e um botão Excluir:

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

Ao sair da conta, destrua todas as chaves por conta (chamadas user_...) para que nada em cache no celular possa ser lido de novo:

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

Observações: Somente as chaves criadas pela ponte são listadas; as chaves internas do próprio app nunca aparecem. Também responde "[]" quando é chamada de uma página que não é do próprio app (outro site carregado no app).

Gerado a partir do runtime do app e de seus arquivos de exemplo a cada build da documentação. Leia em Markdown · Todas as famílias.

Conferido com a ponte atual em 22/10/2026.