Créer un APK gratuit

API du pont JavaScript

Biométrie et keystore

Déverrouillage par empreinte digitale et par visage, et clés protégées par le matériel avec lesquelles la page peut chiffrer mais qu'elle ne peut jamais lire.

AppMintKeystore.decrypt assistant#

window.AppMintKeystore.decrypt(alias, base64)

Exemple

Déchiffre les données créées par AppMintKeystore.encrypt avec la même clé.

Renvoie : un objet, immédiatement (pas une Promise) : {ok:true, base64} (les données en clair) ou {ok:false, error, detail}. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Pour un fichier écrit avec WebToApkFS.writeSealed, relisez-le avec WebToApkFS.readSealed(path, alias) : même forme de résultat.

Relisez un objet JSON scellé, en déverrouillant d'abord si la clé l'exige :

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

Ouvrir une liste d'entrées de journal scellées, en laissant visibles celles qui échouent comme « verrouillées » :

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

Remarques : Après deleteKey, les données sont définitivement perdues ; c'est le but. Une charge modifiée échoue à la vérification GCM. Autres erreurs : bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.deleteKey assistant#

window.AppMintKeystore.deleteKey(alias)

Exemple

Détruit définitivement une clé sécurisée. Tout ce qui a été chiffré avec elle ne pourra plus jamais être lu, par personne.

Renvoie : true (supprimée) ou false (clé inexistante, ou fonctionnalité désactivée), immédiatement. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

Écraser un fichier ne l'efface pas de la mémoire flash. Chiffrez-le, puis détruisez la clé : c'est une vraie suppression :

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

« Tout effacer » dans les paramètres : détruisez toutes les clés créées par cette application, puis videz le stockage de la page :

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

Autodétruisez le coffre après trop de codes PIN d'application erronés :

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

Remarques : il n'y a ni annulation ni récupération. Si isHardwareBacked(alias) valait false, la clé se trouvait dans le logiciel : dites-le avant de promettre à l'utilisateur une suppression irréversible.

AppMintKeystore.encrypt assistant#

window.AppMintKeystore.encrypt(alias, base64)

Exemple

Chiffre des données base64 avec une clé sécurisée. Stockez le résultat où vous voulez : il est inutilisable sans la clé.

Renvoie : un objet, immédiatement (pas une Promise) : {ok:true, base64} (IV + texte chiffré + tag) ou {ok:false, error, detail}. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation, et une clé provenant 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 peut être construit sur authenticateBiometricEx (voir cet exemple).

Pour écrire un FICHIER chiffré de n'importe quelle taille, utilisez WebToApkFS.writeSealed(path, base64, alias) : il chiffre morceau par morceau avec cette même clé.

Scellez un petit objet JSON (une note, un jeton, des paramètres) et conservez-le dans 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);

Créez la clé la première fois qu'elle est nécessaire, puis chiffrez :

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

Remarques : 4 Mo maximum par appel (chunk too large). Erreurs : no-such-key, auth-required, key-invalidated (le verrouillage d'écran a changé : la clé a disparu), bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.generateKey assistant#

window.AppMintKeystore.generateKey(alias, options)

Exemple

Crée (ou remplace) une clé AES-256-GCM dans le Keystore Android. La clé ne quitte jamais le matériel sécurisé ; votre page n'utilise que son nom.

Renvoie : un objet, immédiatement (pas une Promise) : {ok:true, alias, hardwareBacked} ou {ok:false, error, detail}. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation (voir 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);
}

Une clé simple, sans déverrouillage nécessaire :

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

Exigez la puce sécurisée séparée (StrongBox). L'appel échoue au lieu de vous donner une clé plus faible :

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

Remarques : Alias : lettres, chiffres, _ - ., jusqu'à 64 caractères (error:"bad alias" sinon). Utiliser un alias existant remplace la clé, et les données scellées avec l'ancienne sont perdues. Autres erreurs : disabled, bad options json, foreign_origin, unsupported (pas de pont).

AppMintKeystore.hasKey assistant#

window.AppMintKeystore.hasKey(alias)

Exemple

Indique si une clé sécurisée avec cet alias existe.

Renvoie : true / false, immédiatement. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Assurez-vous qu'une clé existe avant le premier enregistrement, en ne la créant qu'une seule fois :

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

Nettoyez les données scellées dont la clé a disparu (supprimée, ou données de l'application restaurées sans elle) :

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

Remarques : Répond false quand les clés sécurisées sont désactivées ; vérifiez donc d'abord isAvailable() si vous devez distinguer « pas encore de clé » de « fonctionnalité absente de cette application ».

AppMintKeystore.isAvailable assistant#

window.AppMintKeystore.isAvailable()

Exemple

Indique si des clés sécurisées adossées au matériel peuvent être utilisées dans cette application.

Renvoie : true / false, immédiatement (pas une Promise). Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation. L'objet window.AppMintKeystore est installé (avant l'exécution de votre premier script) dès que Clés sécurisées ou Accès natif aux dossiers (SAF) est activé ; isAvailable() répond pour les clés sécurisées.

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

Masquez tous les contrôles du coffre sécurisé lorsque la fonctionnalité n'est pas présente dans cette application :

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

Distinguez « fonctionnalité désactivée » de « pas encore de clé » lorsque vous décidez quel écran afficher :

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

Remarques : L'assistant est ajouté une fois la page chargée ; ne le testez donc pas dans un script exécuté pendant l'analyse de la page, mais dans un gestionnaire de clic ou après load. Dans un navigateur, window.AppMintKeystore n'existe pas ; la vérification ci-dessus renvoie false sans lever d'exception.

AppMintKeystore.isHardwareBacked assistant#

window.AppMintKeystore.isHardwareBacked(alias)

Exemple

Indique si une clé se trouve dans le matériel sécurisé du téléphone (TEE ou StrongBox) plutôt que dans le logiciel.

Renvoie : true / false, immédiatement. false lorsque la clé n'existe pas. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Ne conservez un secret de grande valeur (une phrase de récupération de portefeuille, un code de récupération) que si sa clé se trouve dans du matériel sécurisé :

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

Lister où se trouve chaque clé, pour un rapport d'assistance :

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

Remarques : generateKey renvoie aussi hardwareBacked juste après avoir créé la clé.

AppMintKeystore.listKeys assistant#

window.AppMintKeystore.listKeys()

Exemple

Liste les alias de chaque clé sécurisée créée par vos pages.

Renvoie : un tableau de chaînes, trié, immédiatement, par ex. ['notes', 'vault']. [] lorsqu'il n'y en a aucune ou que la fonctionnalité est désactivée. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Détruisez chaque clé lors de « Effacer toutes mes données », ce qui rend illisible tout ce qui a été scellé avec elles :

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

Conservez une clé par utilisateur connecté et supprimez les clés des utilisateurs déconnectés :

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

Remarques : seules les clés créées via le pont sont listées ; les clés internes propres à l'application ne sont jamais affichées.

authenticateBiometric pont#

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

WebToApk.isBiometricAvailable() -> "available" | "no_hardware" | "not_enrolled" | "unavailable" WebToApk.authenticateBiometric(id, title, subtitle) Le résultat est transmis à la même table de rappels __webToApkAuth que celle utilisée par le pont 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');

Décrit par son groupe, Pont empreinte digitale / biométrie, plutôt que seul.

Exemple

Affiche l'invite d'empreinte digitale du système avec un titre et un sous-titre. La forme simple de authenticateBiometricEx (utilisez celle-ci pour l'option PIN, un libellé Annuler personnalisé ou la biométrie « forte »).

Renvoie : rien. La réponse arrive plus tard : la coque appelle window.__webToApkAuth[callbackId](json) avec '{"ok":true}' ou '{"ok":false,"error":"<code>"}', puis supprime cette entrée. Nécessite : Empreinte digitale cochée lors de la compilation (sans elle : error:"permission_missing").

Enregistrez d'abord le rappel, puis appelez :

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

Confirmez d'abord une action risquée avec l'empreinte digitale (un titre vide affiche le nom de l'application) :

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

Reverrouillez l'application lorsqu'elle revient après plus d'une minute en arrière-plan :

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

Remarques : Un titre vide utilise le nom de l'application. Un mauvais doigt NE met PAS fin à l'invite : le rappel n'est appelé qu'en cas de réussite, d'annulation ou d'erreur réelle. Les codes d'erreur sont les mêmes que pour authenticateBiometricEx. Cela prouve que le propriétaire du téléphone est présent ; cela ne prouve pas son identité auprès d'un serveur.

authenticateBiometricEx pont#

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

La forme complète. [optionsJson] accepte : title, subtitle, description, negativeButtonText, allowDeviceCredential (repli par code PIN/schéma/mot de passe), strong (exige BIOMETRIC_STRONG - à utiliser pour tout ce qui touche au paiement). Les erreurs sont des codes distincts, pas un simple booléen. Avant toute invite : no_hardware | none_enrolled | unavailable (la vérification d'état n'a pas pu s'exécuter - voir isBiometricAvailable). Depuis l'invite système d'Android 9+ : user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error, et pour un code qu'Android ajoutera plus tard, son propre texte de message (ou `error_<code>`). permission_missing quand la compilation n'a pas l'autorisation Fingerprint. Sur Android 7-8 (API 24-27, empreinte uniquement), une erreur est le texte de message du téléphone tel que le fabricant l'a écrit - pas un code fixe.

Exemple

Affiche l'invite d'empreinte digitale / de visage du système avec toutes les options : PIN ou schéma comme second accès, libellé Annuler personnalisé, et biométrie « forte » uniquement.

Renvoie : rien. La réponse arrive plus tard : la coque appelle window.__webToApkAuth[callbackId](json) avec '{"ok":true}' ou '{"ok":false,"error":"<code>"}', puis supprime cette entrée. Nécessite : Empreinte digitale cochée lors de la compilation (sans elle : error:"permission_missing").

Une petite encapsulation Promise réutilisable :

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

Déverrouillez l'application : empreinte digitale, visage, ou code PIN/schéma du téléphone :

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

Confirmer un paiement : biométrie forte uniquement, avec votre propre libellé d'annulation :

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

Déverrouillez une clé Keystore créée avec requireAuth, puis réessayez :

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

Remarques : codes d'erreur : user_cancel, none_enrolled, no_hardware, hw_unavailable, lockout (attendre et réessayer), lockout_permanent (l'utilisateur doit d'abord déverrouiller le téléphone avec le PIN), no_device_credential, timeout, permission_missing, unavailable, et plus rarement no_space, unable_to_process, vendor_error. Sous Android 7-8, la coque affiche sa propre boîte de dialogue et l'erreur est le texte du message du système. allowDeviceCredential et negativeButtonText ne se combinent pas : avec l'option PIN, le système dessine son propre bouton. Un mauvais doigt ne met pas fin à l'invite.

decryptWithKey pont#

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

Inverse [encryptWithKey]. Échoue avec `error:"no-such-key"` une fois la clé supprimée : la charge est alors irrécupérable par conception. @return JSON `{ok, base64, error}`.

Exemple

Déchiffre les données produites par encryptWithKey avec la même clé Keystore. La plupart des pages utilisent AppMintKeystore.decrypt(alias, base64), qui renvoie l'objet analysé.

Renvoie : une chaîne JSON, immédiatement : {"ok":true,"base64":"<plain data>"} ou {"ok":false,"error":"...","detail":"..."}. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Pour une clé créée avec requireAuth:true : sur auth-required, affichez l'invite de déverrouillage, puis déchiffrez à nouveau :

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

Affichez une photo chiffrée : déchiffrez en octets, puis affichez-la via une URL 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;
}

Remarques : Une fois la clé supprimée (deleteKey), les données ne pourront plus jamais être déchiffrées, par conception. Une charge modifiée ou endommagée échoue (le tag GCM ne correspond pas) ; l'error est alors le message de la plateforme, pas l'un des codes ci-dessus. Autres codes : bad base64, disabled, foreign_origin.

deleteKey pont#

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

Détruit [alias] définitivement. Tout ce qui a été chiffré avec devient indéchiffrable, y compris par cette application et par toute personne détenant l'appareil.

Exemple

Détruit définitivement une clé Keystore. Tout ce qui a été chiffré avec elle ne pourra plus jamais être lu : ni par vous, ni par l'utilisateur, ni par quiconque détient le téléphone. La plupart des pages utilisent AppMintKeystore.deleteKey(alias), qui renvoie un vrai booléen.

Renvoie : la CHAÎNE "true" (supprimée) ou "false" (clé inexistante, clés sécurisées désactivées, ou page ne faisant pas partie de l'application), immédiatement. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

C'est la seule vraie « suppression sécurisée » sur un téléphone : écraser un fichier ne l'efface pas de la mémoire flash, détruire la clé, si.

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

Faites pivoter une clé : ouvrez les données avec l'ancienne clé, scellez-les avec une nouvelle, puis détruisez l'ancienne clé :

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
}

« Oublier mon compte sur ce téléphone » à la déconnexion : détruisez la clé du compte, puis supprimez ses données en cache :

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

Remarques : Vérifiez isKeyHardwareBacked avant de promettre à un utilisateur que la suppression est irréversible : une clé uniquement logicielle est plus faible.

encryptWithKey pont#

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

Chiffre le base64 [base64Plaintext] avec [alias]. Chaque appel génère son propre IV, donc un gros fichier est chiffré morceau par morceau ; les morceaux sont indépendants et peuvent être déchiffrés dans n'importe quel ordre. @return JSON `{ok, base64, error}` où `base64` est `IV || ciphertext || tag`.

Exemple

Chiffre des données base64 avec une clé Keystore. La plupart des pages utilisent AppMintKeystore.encrypt(alias, base64), qui renvoie l'objet analysé.

Renvoie : une chaîne JSON, immédiatement : {"ok":true,"base64":"<IV + ciphertext + tag>"} ou {"ok":false,"error":"...","detail":"..."}. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation, et une clé créée avec generateKey.

L'entrée est en base64. Pour du texte, convertissez-le d'abord en 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;
}

Scellez un objet entier (une session, des paramètres) en JSON avant de le stocker :

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

Chiffrer une photo choisie par l'utilisateur (données binaires, jusqu'à 4 Mo par appel) :

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

Remarques : Chaque appel utilise un nouvel IV, donc le même texte donne un résultat différent à chaque fois. 4 Mo maximum par appel (error:"chunk too large ...") ; pour des fichiers plus gros, chiffrez morceau par morceau, ou utilisez WebToApkFS.writeSealed. Codes d'erreur : no-such-key, auth-required, key-invalidated (l'utilisateur a changé le verrouillage d'écran : la clé est définitivement perdue), bad base64, disabled, foreign_origin.

generateKey pont#

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

Crée (ou remplace) une clé AES-256-GCM nommée [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` lie la clé au verrouillage de l'appareil ; `strongBox` échoue franchement sur les appareils sans élément sécurisé plutôt que de renvoyer une clé plus faible que celle demandée. @return JSON `{ok, alias, hardwareBacked, error}`.

Exemple

Crée (ou remplace) une clé AES-256-GCM dans le Keystore Android. La page obtient un nom pour la clé, jamais la clé elle-même. La plupart des pages utilisent AppMintKeystore.generateKey(alias, options), qui gère le JSON à votre place.

Renvoie : une chaîne JSON, immédiatement : {"ok":true,"alias":"vault","hardwareBacked":true} ou {"ok":false,"error":"...","detail":"..."}. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Une clé simple sans règle de déverrouillage (les options peuvent être '{}'):

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

Créez la clé une seule fois, la première fois qu'elle est nécessaire : appeler à nouveau generateKey la remplacerait et rendrait orphelin tout ce qui est déjà scellé :

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

Exigez la puce sécurisée séparée pour une clé de portefeuille, et dites clairement à l'utilisateur quand le téléphone n'en a pas :

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

Remarques : l'alias ne peut contenir que des lettres, chiffres, _, -, . et au plus 64 caractères, sinon error:"bad alias". L'appeler à nouveau avec le même alias REMPLACE la clé, et les données scellées avec l'ancienne clé ne peuvent plus être lues. Codes d'erreur : disabled (Clés sécurisées non activées), bad options json, strongbox-unavailable (vous avez demandé strongBox:true et le téléphone n'a pas de puce sécurisée, ou il est antérieur à Android 9), foreign_origin (appelé depuis une page qui n'est pas celle de l'application).

hasKey pont#

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

Vrai lorsqu'une clé nommée [alias] existe dans le keystore de cette application.

Exemple

Indique si une clé Keystore avec cet alias existe. La plupart des pages utilisent AppMintKeystore.hasKey(alias), qui renvoie un vrai booléen.

Renvoie : la CHAÎNE "true" ou "false", immédiatement. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation (sans cela, la réponse est toujours "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();
}

Choisissez l'écran de démarrage : déverrouiller un coffre existant, ou en créer un, et nettoyer les données scellées dont la clé a disparu :

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

Une clé par compte connecté, créée la première fois que ce compte se connecte sur ce téléphone :

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

Remarques : la réponse est une chaîne, donc if (WebToApk.hasKey('vault')) est toujours vrai : comparez avec === 'true'. Vous ne voyez que les clés que cette application a créées via le pont ; les clés propres de la coque sont masquées.

isBiometricAvailable pont#

window.WebToApk.isBiometricAvailable(): String

"available" | "no_hardware" | "not_enrolled" | "unavailable". Android 10+ compte le visage, l'iris et l'empreinte digitale ; Android 6-9 uniquement le capteur d'empreintes. "unavailable" = le capteur est temporairement inutilisable, ou la vérification n'a pas pu s'exécuter (par ex. la compilation n'a pas activé l'autorisation Empreinte digitale). Voir [BiometricStatus].

Exemple

Indique si le téléphone peut effectuer un déverrouillage biométrique (visage, iris ou empreinte digitale) en ce moment. Vérifiez-le avant d'afficher un bouton de déverrouillage.

Renvoie : une chaîne, immédiatement : "available", "no_hardware" (pas de capteur), "not_enrolled" (capteur, mais rien d'enregistré, ou pas de verrouillage d'écran défini) ou "unavailable" (le capteur est temporairement inutilisable, ou la vérification n'a pas pu s'exécuter : par exemple la compilation n'a pas coché Empreinte digitale). Nécessite : Empreinte digitale cochée lors de la compilation : sans elle, Android refuse de répondre et le résultat est "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 interrupteur de paramètres « Déverrouiller avec l'empreinte digitale » grisé avec la raison lorsqu'il ne peut pas fonctionner :

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

Vérifiez à nouveau quand l'utilisateur revient des paramètres du téléphone, au cas où il vient d'ajouter une empreinte :

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

Remarques : sous Android 10 et versions ultérieures, la vérification compte le visage, l'iris et l'empreinte digitale : un téléphone avec déverrouillage facial uniquement répond donc "available". Sous Android 6-9, elle ne regarde que le capteur d'empreintes : un téléphone avec visage uniquement y répond "no_hardware" ; avec allowDeviceCredential:true dans authenticateBiometricEx, l'utilisateur peut tout de même déverrouiller avec le PIN, le schéma ou le mot de passe du téléphone (Android 11+).

isKeyHardwareBacked pont#

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

Vrai lorsque [alias] se trouve dans le TEE ou un élément sécurisé plutôt que dans le logiciel. À vérifier avant de promettre à un utilisateur que la destruction d'une clé est irréversible.

Exemple

Indique si une clé se trouve dans le matériel sécurisé du téléphone (TEE ou puce StrongBox) plutôt que dans le logiciel. La plupart des pages utilisent AppMintKeystore.isHardwareBacked(alias), qui renvoie un vrai booléen.

Renvoie : la CHAÎNE "true" ou "false", immédiatement. "false" aussi quand la clé n'existe pas. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation.

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

Refusez de conserver un secret de grande valeur sur un téléphone qui ne peut détenir la clé que de façon logicielle :

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

Envoyez à votre serveur un petit résumé de la sécurité de l'appareil lors de la connexion, pour qu'il décide ce qu'il autorise :

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

Remarques : n'écrivez « matériel » dans votre interface que si c'est "true".

isKeystoreEnabled pont#

window.WebToApk.isKeystoreEnabled(): String

Indique si le créateur a activé le pont keystore pour cette application.

Exemple

Indique si le créateur a activé le pont Keystore pour cette application. La plupart des pages utilisent AppMintKeystore.isAvailable(), qui renvoie un vrai booléen.

Renvoie : la CHAÎNE "true" ou "false", immédiatement. Nécessite : rien pour l'appeler ; elle ne répond "true" que si Clés sécurisées (chiffrement) était activé lors de la compilation.

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

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

Ne proposez « Se souvenir de moi » que si le jeton peut être stocké chiffré :

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

Distinguer « ouvert dans un navigateur » de « application compilée sans clés sécurisées » pendant vos tests :

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

Remarques : lorsqu'elle vaut "false", toutes les autres méthodes de clé répondent "false", "[]" ou {ok:false, error:"disabled"}. Masquez la fonctionnalité au lieu de proposer un bouton qui ne peut pas fonctionner.

listKeys pont#

window.WebToApk.listKeys(): String

Tableau JSON de chaque alias de clé que cette page a créé.

Exemple

Liste les alias de chaque clé Keystore créée par les pages de cette application. La plupart des pages utilisent AppMintKeystore.listKeys(), qui renvoie le tableau analysé.

Renvoie : une chaîne de tableau JSON, triée par nom, immédiatement : '["notes","vault"]'. Nécessite : Clés sécurisées (chiffrement) activées lors de la compilation (sans cela : "[]").

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

Un écran de paramètres « Clés de sécurité » qui liste chaque clé, où elle se trouve, et un bouton Supprimer :

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

À la déconnexion, détruisez toutes les clés propres au compte (nommées user_...) afin que plus rien de ce qui est en cache sur le téléphone ne puisse être lu :

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

Remarques : Seules les clés créées via le pont sont listées ; les clés internes de l'application ne sont jamais affichées. Répond aussi "[]" lorsqu'il est appelé depuis une page qui n'est pas celle de l'application (un autre site chargé dans l'application).

Généré à partir de l'environnement d'exécution de l'application et de ses fichiers d'exemple à chaque compilation de la documentation. Lire en Markdown · Toutes les familles.

Vérifié par rapport au pont publié le 22/10/2026.