ساخت APK رایگان

API پل جاوااسکریپت

زیست‌سنجی و keystore

باز کردن قفل با اثر انگشت و چهره، و کلیدهای پشتیبانی‌شده با سخت‌افزار که صفحه می‌تواند با آن‌ها رمزگذاری کند اما هرگز نمی‌تواند بخواند.

AppMintKeystore.decrypt کمک‌کننده#

window.AppMintKeystore.decrypt(alias, base64)

نمونه

داده‌هایی را که AppMintKeystore.encrypt ساخته با همان کلید رمزگشایی می‌کند.

مقدار بازگشتی: یک شیء، بلافاصله (نه Promise): {ok:true, base64} (داده‌ی ساده) یا {ok:false, error, detail}. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

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

برای فایلی که با WebToApkFS.writeSealed نوشته شده، آن را با WebToApkFS.readSealed(path, alias) بخوانید - ساختار نتیجه یکی است.

یک شیء JSON مهر و موم‌شده را بخوانید، و اگر کلید لازم دارد اول قفلش را باز کنید:

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

فهرستی از یادداشت‌های مهروموم‌شده‌ی دفترچه را باز کنید و مواردی را که شکست می‌خورند به‌صورت «قفل‌شده» نمایان نگه دارید:

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

نکته‌ها: بعد از deleteKey داده برای همیشه از بین می‌رود؛ هدف همین است. محتوایی که تغییر کرده باشد در بررسی GCM رد می‌شود. خطاهای دیگر: bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.deleteKey کمک‌کننده#

window.AppMintKeystore.deleteKey(alias)

نمونه

یک کلید امن را برای همیشه نابود می‌کند. هر چیزی که با آن رمزنگاری شده باشد، دیگر هرگز توسط هیچ‌کس قابل خواندن نیست.

مقدار بازگشتی: true (حذف شد) یا false (چنین کلیدی نیست، یا ویژگی خاموش است)، بلافاصله. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

بازنویسی یک فایل آن را روی حافظه‌ی فلش پاک نمی‌کند. رمزگذاری‌اش کنید، بعد کلید را نابود کنید - این حذف واقعی است:

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

«پاک کردن همه‌چیز» در تنظیمات: همه‌ی کلیدهایی را که این اپلیکیشن ساخته نابود کنید، سپس حافظه‌ی صفحه را پاک کنید:

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

بعد از چند پین اشتباه اپلیکیشن، گاوصندوق را خودمخرب کنید:

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

نکته‌ها: بازگشت و بازیابی وجود ندارد. اگر isHardwareBacked(alias) برابر false بود، کلید در نرم‌افزار زندگی می‌کرد - قبل از اینکه به کاربر قول حذف بازگشت‌ناپذیر بدهید این را بگویید.

AppMintKeystore.encrypt کمک‌کننده#

window.AppMintKeystore.encrypt(alias, base64)

نمونه

داده‌ی base64 را با یک کلید امن رمزگذاری می‌کند. نتیجه را هرجا ذخیره کنید - بدون کلید بی‌فایده است.

بازگشتی: یک شیء، بلافاصله (نه Promise): {ok:true, base64} (IV + متن رمزنگاری‌شده + تگ) یا {ok:false, error, detail}. نیازمند: روشن بودن کلیدهای امن (رمزنگاری) هنگام ساخت، و یک کلید از 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 را می‌توان روی authenticateBiometricEx ساخت (به آن نمونه نگاه کنید).

برای نوشتن یک فایل رمزنگاری‌شده با هر اندازه از WebToApkFS.writeSealed(path, base64, alias) استفاده کنید - این تابع با همین کلید تکه‌تکه رمزنگاری می‌کند.

یک شیء JSON کوچک (یادداشت، توکن، تنظیمات) را مهر و موم کنید و در 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);

کلید را اولین باری که لازم است بسازید، بعد رمزگذاری کنید:

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

نکته‌ها: حداکثر 4 MB در هر فراخوانی (chunk too large). خطاها: no-such-key، auth-required، key-invalidated (قفل صفحه تغییر کرد - کلید رفته است)، bad base64, disabled, foreign_origin, unsupported.

AppMintKeystore.generateKey کمک‌کننده#

window.AppMintKeystore.generateKey(alias, options)

نمونه

یک کلید AES-256-GCM در Android Keystore می‌سازد (یا جایگزین می‌کند). کلید هرگز سخت‌افزار امن را ترک نمی‌کند؛ صفحه‌ی شما فقط از نامش استفاده می‌کند.

مقدار بازگشتی: یک شیء، بلافاصله (نه Promise): {ok:true, alias, hardwareBacked} یا {ok:false, error, detail}. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد (ببینید 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);
}

یک کلید ساده، بدون نیاز به باز کردن قفل:

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

تراشه‌ی امن جداگانه (StrongBox) را الزامی کنید. به‌جای دادن کلید ضعیف‌تر، با خطا متوقف می‌شود:

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

نکته‌ها: نام مستعار: حرف، رقم، _ - .، تا ۶۴ نویسه (در غیر این صورت error:"bad alias"). استفاده از نام مستعار موجود، کلید را جایگزین می‌کند و داده‌ی مهروموم‌شده با کلید قبلی از دست می‌رود. خطاهای دیگر: disabled، bad options json، foreign_origin، unsupported (بدون پل).

AppMintKeystore.hasKey کمک‌کننده#

window.AppMintKeystore.hasKey(alias)

نمونه

مشخص می‌کند که آیا کلید امنی با این نام مستعار وجود دارد یا نه.

مقدار بازگشتی: true / false، بلافاصله. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

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

قبل از اولین ذخیره مطمئن شوید کلید وجود دارد و فقط یک بار آن را بسازید:

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

داده‌ی مهر و موم‌شده‌ای را که کلیدش رفته پاک کنید (حذف شده، یا داده‌ی اپلیکیشن بدون آن بازیابی شده):

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

وقتی کلیدهای امن خاموش باشد false پاسخ می‌دهد، پس اگر لازم است «هنوز کلیدی نیست» را از «قابلیت در این اپلیکیشن نیست» تشخیص دهید، ابتدا isAvailable() را بررسی کنید. نکته‌ها:

AppMintKeystore.isAvailable کمک‌کننده#

window.AppMintKeystore.isAvailable()

نمونه

مشخص می‌کند که آیا کلیدهای امن مبتنی بر سخت‌افزار در این اپلیکیشن قابل استفاده‌اند یا نه.

بازگشتی: true / false، بلافاصله (نه Promise). نیازمند: روشن بودن کلیدهای امن (رمزنگاری) هنگام ساخت. شیء window.AppMintKeystore هر زمان که کلیدهای امن یا دسترسی به پوشه‌ی بومی (SAF) روشن باشد (پیش از اجرای اولین اسکریپت شما) نصب می‌شود؛ isAvailable() برای کلیدهای امن پاسخ می‌دهد.

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

وقتی این قابلیت در اپلیکیشن نیست، همه‌ی کنترل‌های گاوصندوق امن را پنهان کنید:

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

«ویژگی خاموش» را از «هنوز کلیدی نیست» تشخیص دهید وقتی تصمیم می‌گیرید کدام صفحه را نشان دهید:

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

نکته‌ها: این کمکی بعد از بارگذاری صفحه اضافه می‌شود، پس آن را در اسکریپتی که هنوز صفحه در حال تجزیه است بررسی نکنید - در یک کنترل‌کننده‌ی کلیک یا بعد از load بررسی کنید. در مرورگر window.AppMintKeystore وجود ندارد؛ بررسی بالا بدون پرتاب خطا false برمی‌گرداند.

AppMintKeystore.isHardwareBacked کمک‌کننده#

window.AppMintKeystore.isHardwareBacked(alias)

نمونه

مشخص می‌کند که آیا کلید در سخت‌افزار امن گوشی (TEE یا StrongBox) قرار دارد یا در نرم‌افزار.

مقدار بازگشتی: true / false، بلافاصله. false وقتی کلید وجود ندارد. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

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

یک راز باارزش (عبارت بازیابی کیف پول، کد بازیابی) را فقط وقتی نگه دارید که کلیدش در سخت‌افزار امن باشد:

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

محل قرارگیری هر کلید را برای گزارش پشتیبانی فهرست کنید:

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

نکته‌ها: generateKey بلافاصله بعد از ساختن کلید hardwareBacked هم برمی‌گرداند.

AppMintKeystore.listKeys کمک‌کننده#

window.AppMintKeystore.listKeys()

نمونه

نام‌های مستعار هر کلید امنی را که صفحه‌های شما ساخته‌اند فهرست می‌کند.

مقدار بازگشتی: یک آرایه‌ی رشته‌ها، مرتب‌شده، بلافاصله، مثلاً ['notes', 'vault']. [] وقتی چیزی نیست یا ویژگی خاموش است. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

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

هنگام «پاک کردن همه‌ی داده‌های من» هر کلید را نابود کنید، که هر چیز مهر و موم‌شده با آن‌ها را ناخواندنی می‌کند:

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

برای هر کاربر واردشده یک کلید نگه دارید و کلیدهای کاربرانی را که خارج شده‌اند حذف کنید:

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

نکته‌ها: فقط کلیدهایی که از طریق پل ساخته شده‌اند فهرست می‌شوند؛ کلیدهای داخلی خود اپلیکیشن هرگز نشان داده نمی‌شوند.

authenticateBiometric پل#

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

WebToApk.isBiometricAvailable() -> "available" | "no_hardware" | "not_enrolled" | "unavailable" WebToApk.authenticateBiometric(id, title, subtitle) نتیجه به همان جدول فراخوانی __webToApkAuth که پل ورود با Google استفاده می‌کند تحویل داده می‌شود: 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');

با گروهش، پل اثر انگشت / بیومتریک، توضیح داده می‌شود، نه جداگانه.

نمونه

پنجره‌ی اثر انگشت سیستم را با عنوان و زیرعنوان نشان می‌دهد. شکل ساده‌ی authenticateBiometricEx (برای گزینه‌ی پین، برچسب سفارشی «لغو» یا بیومتریک «قوی» از آن استفاده کنید).

مقدار بازگشتی: چیزی؛ پاسخ بعداً می‌رسد: پوسته window.__webToApkAuth[callbackId](json) را با '{"ok":true}' یا '{"ok":false,"error":"<code>"}' صدا می‌زند، بعد آن مورد را حذف می‌کند. نیازمندی: Fingerprint هنگام ساخت تیک خورده باشد (بدون آن: error:"permission_missing").

ابتدا callback را ثبت کنید، بعد صدا بزنید:

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

یک اقدام پرخطر را اول با اثر انگشت تأیید کنید (عنوان خالی نام اپلیکیشن را نشان می‌دهد):

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

وقتی اپلیکیشن بعد از بیش از یک دقیقه در پس‌زمینه برمی‌گردد دوباره قفلش کنید:

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

نکته‌ها: عنوان خالی از نام اپلیکیشن استفاده می‌کند. انگشت اشتباه پنجره را تمام نمی‌کند - فراخوانی فقط در موفقیت، لغو یا خطای واقعی اجرا می‌شود. کدهای خطا همان‌هایی‌اند که در authenticateBiometricEx هست. این نشان می‌دهد صاحب گوشی حضور دارد؛ برای یک سرور ثابت نمی‌کند او چه کسی است.

authenticateBiometricEx پل#

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

شکل کامل. [optionsJson] این‌ها را می‌پذیرد: title، subtitle، description، negativeButtonText، allowDeviceCredential (جایگزین پین/الگو/گذرواژه)، strong (الزام BIOMETRIC_STRONG - برای هر چیزی که شکل پرداخت دارد استفاده کنید). خطاها کدهای جداگانه‌اند، نه یک مقدار بولین. پیش از هر درخواست: no_hardware | none_enrolled | unavailable (بررسی وضعیت اجرا نشد - isBiometricAvailable را ببینید). از پنجره‌ی سیستمی اندروید ۹ به بالا: user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error، و برای کدی که اندروید بعداً اضافه کند، متن پیام خودش (یا `error_<code>`). permission_missing وقتی ساخت، مجوز Fingerprint را ندارد. در اندروید ۷ تا ۸ (API 24 تا 27، فقط اثر انگشت) خطا همان متن پیام خود گوشی است که سازنده نوشته - نه یک کد ثابت.

نمونه

پنجره‌ی اثر انگشت / چهره‌ی سیستم را با همه‌ی گزینه‌ها نشان می‌دهد: پین یا الگو به‌عنوان راه دوم ورود، برچسب سفارشی «لغو»، و فقط بیومتریک «قوی».

مقدار بازگشتی: چیزی؛ پاسخ بعداً می‌رسد: پوسته window.__webToApkAuth[callbackId](json) را با '{"ok":true}' یا '{"ok":false,"error":"<code>"}' صدا می‌زند، بعد آن مورد را حذف می‌کند. نیازمندی: Fingerprint هنگام ساخت تیک خورده باشد (بدون آن: error:"permission_missing").

یک پوشاننده‌ی کوچک Promise که می‌توانید دوباره استفاده کنید:

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

اپلیکیشن را باز کنید - اثر انگشت، چهره، یا پین/الگوی گوشی:

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

تأیید یک پرداخت - فقط با زیست‌سنجی قوی، با برچسب «لغو» اختصاصی خودتان:

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

یک کلید Keystore ساخته‌شده با requireAuth را باز کنید، بعد دوباره تلاش کنید:

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

نکته‌ها: کدهای خطا: user_cancel، none_enrolled، no_hardware، hw_unavailable، lockout (صبر کنید و دوباره تلاش کنید)، lockout_permanent (کاربر باید ابتدا گوشی را با پین باز کند)، no_device_credential، timeout، permission_missing، unavailable، و به‌ندرت no_space، unable_to_process، vendor_error. در Android 7-8 پوسته پنجره‌ی خودش را نشان می‌دهد و خطا متن پیام سیستم است. allowDeviceCredential و negativeButtonText با هم نمی‌خوانند: با گزینه‌ی پین سیستم دکمه‌ی خودش را رسم می‌کند. انگشت اشتباه پنجره را تمام نمی‌کند.

decryptWithKey پل#

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

عکس‌العمل [encryptWithKey] است. وقتی کلید حذف شده باشد با `error:"no-such-key"` شکست می‌خورد - در این حالت محتوا طبق طراحی بازیابی‌شدنی نیست. @return یک JSON به شکل `{ok, base64, error}`.

نمونه

داده‌ی ساخته‌شده توسط encryptWithKey را با همان کلید Keystore رمزگشایی می‌کند. بیشتر صفحه‌ها از AppMintKeystore.decrypt(alias, base64) استفاده می‌کنند که شیء تجزیه‌شده را برمی‌گرداند.

مقدار بازگشتی: یک رشته‌ی JSON، بلافاصله: {"ok":true,"base64":"<plain data>"} یا {"ok":false,"error":"...","detail":"..."}. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

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

برای کلیدی ساخته‌شده با requireAuth:true: روی auth-required، پنجره‌ی باز کردن قفل را نشان دهید، بعد دوباره رمزگشایی کنید:

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

یک عکس رمزگذاری‌شده را نشان دهید: به بایت رمزگشایی کنید، بعد از طریق یک blob URL نمایش دهید:

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

نکته‌ها: وقتی کلید حذف شد (deleteKey)، داده را دیگر هرگز نمی‌توان رمزگشایی کرد - طبق طراحی. محتوای تغییرکرده یا آسیب‌دیده شکست می‌خورد (تگ GCM مطابقت ندارد)؛ در این حالت error پیام خود پلتفرم است، نه یکی از کدهای بالا. کدهای دیگر: bad base64, disabled, foreign_origin.

deleteKey پل#

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

[alias] را برای همیشه نابود می‌کند. هر چیزی که با آن رمزنگاری شده باشد غیرقابل رمزگشایی می‌شود - حتی توسط همین اپلیکیشن و توسط هر کسی که دستگاه را در دست دارد.

نمونه

یک کلید Keystore را برای همیشه نابود می‌کند. هر چیزی که با آن رمزگذاری شده هرگز دوباره خوانده نمی‌شود - نه توسط شما، نه کاربر، نه هر کسی که گوشی دستش است. بیشتر صفحه‌ها از AppMintKeystore.deleteKey(alias) استفاده می‌کنند که یک boolean واقعی برمی‌گرداند.

بازگشتی: رشته‌ی "true" (حذف شد) یا "false" (چنین کلیدی نیست، کلیدهای امن خاموش است یا صفحه، صفحه‌ی خود اپلیکیشن نیست)، بلافاصله. نیازمند: روشن بودن کلیدهای امن (رمزنگاری) هنگام ساخت.

این تنها «حذف امن» واقعی روی گوشی است: بازنویسی یک فایل آن را روی حافظه‌ی فلش پاک نمی‌کند، نابود کردن کلید پاک می‌کند.

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

یک کلید را بچرخانید: داده را با کلید قدیمی باز کنید، با یک کلید جدید مهر و موم کنید، بعد کلید قدیمی را نابود کنید:

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
}

«مرا روی این گوشی فراموش کن» هنگام خروج از حساب: کلید حساب را نابود کنید، سپس داده‌های ذخیره‌شده‌ی آن را حذف کنید:

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

نکته‌ها: پیش از اینکه به کاربر قول بدهید حذف بازگشت‌ناپذیر است، isKeyHardwareBacked را بررسی کنید - کلیدی که فقط نرم‌افزاری است ضعیف‌تر است.

encryptWithKey پل#

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

متن base64 [base64Plaintext] را با [alias] رمزنگاری می‌کند. هر فراخوانی IV مخصوص خودش را می‌سازد، پس یک فایل بزرگ تکه‌تکه رمزنگاری می‌شود - تکه‌ها مستقل‌اند و به هر ترتیبی می‌توان رمزگشایی‌شان کرد. @return یک JSON به شکل `{ok, base64, error}` که در آن `base64` برابر `IV || ciphertext || tag` است.

نمونه

داده‌ی base64 را با یک کلید Keystore رمزگذاری می‌کند. بیشتر صفحه‌ها از AppMintKeystore.encrypt(alias, base64) استفاده می‌کنند که شیء تجزیه‌شده را برمی‌گرداند.

مقدار بازگشتی: یک رشته‌ی JSON، بلافاصله: {"ok":true,"base64":"<IV + ciphertext + tag>"} یا {"ok":false,"error":"...","detail":"..."}. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد، و کلیدی ساخته‌شده با generateKey.

ورودی base64 است. برای متن، ابتدا آن را به 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;
}

یک شیء کامل (نشست، تنظیمات) را قبل از ذخیره به‌صورت JSON مهر و موم کنید:

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

رمزنگاری عکسی که کاربر انتخاب می‌کند (داده‌ی دودویی، تا ۴ مگابایت در هر فراخوانی):

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

نکته‌ها: هر فراخوانی از IV تازه استفاده می‌کند، پس یک متن مشابه هر بار نتیجه‌ی متفاوتی می‌دهد. حداکثر ۴ مگابایت در هر فراخوانی (error:"chunk too large ...")؛ برای فایل‌های بزرگ‌تر تکه‌تکه رمزنگاری کنید یا از WebToApkFS.writeSealed استفاده کنید. کدهای خطا: no-such-key، auth-required، key-invalidated (کاربر قفل صفحه را تغییر داده - کلید برای همیشه از بین رفته)، bad base64, disabled, foreign_origin.

generateKey پل#

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

یک کلید AES-256-GCM به نام [alias] می‌سازد (یا جایگزین می‌کند). @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` کلید را به قفل دستگاه می‌بندد؛ `strongBox` در دستگاه‌های بدون عنصر امن کاملاً شکست می‌خورد، به‌جای اینکه کلیدی ضعیف‌تر از خواسته‌شده بدهد. @return JSON `{ok, alias, hardwareBacked, error}`.

نمونه

یک کلید AES-256-GCM در Android Keystore می‌سازد (یا جایگزین می‌کند). صفحه نامی برای کلید می‌گیرد، نه خود کلید. بیشتر صفحه‌ها از AppMintKeystore.generateKey(alias, options) استفاده می‌کنند که کار JSON را برایتان انجام می‌دهد.

مقدار بازگشتی: یک رشته‌ی JSON، بلافاصله: {"ok":true,"alias":"vault","hardwareBacked":true} یا {"ok":false,"error":"...","detail":"..."}. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد.

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

یک کلید ساده بدون قاعده‌ی قفل‌گشایی (گزینه‌ها می‌توانند '{}'):

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

کلید را یک بار بسازید، اولین باری که لازم است - صدا زدن دوباره‌ی generateKey آن را جایگزین می‌کند و هر چیز مهر و موم‌شده را یتیم می‌کند:

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

برای کلید کیف پول، تراشه‌ی امن جداگانه را الزامی کنید و وقتی گوشی آن را ندارد، صریح به کاربر بگویید:

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

نکته‌ها: نام مستعار فقط می‌تواند شامل حروف، ارقام، _، -، . و حداکثر 64 نویسه باشد، وگرنه error:"bad alias". صدا زدن دوباره با همان نام مستعار کلید را جایگزین می‌کند و داده‌ی مهر و موم‌شده با کلید قدیمی دیگر خوانده نمی‌شود. کدهای خطا: disabled (کلیدهای امن روشن نیست)، bad options json، strongbox-unavailable (strongBox:true خواستید و گوشی تراشه‌ی امن ندارد یا از Android 9 قدیمی‌تر است)، foreign_origin (از صفحه‌ای که مال خود اپلیکیشن نیست صدا زده شد).

hasKey پل#

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

درست است وقتی کلیدی به نام [alias] در keystore این اپلیکیشن وجود دارد.

نمونه

مشخص می‌کند که آیا کلیدی در Keystore با این نام مستعار وجود دارد یا نه. بیشتر صفحه‌ها از AppMintKeystore.hasKey(alias) استفاده می‌کنند که یک مقدار بولین واقعی برمی‌گرداند.

مقدار بازگشتی: رشته‌ی "true" یا "false"، بلافاصله. نیازمندی: کلیدهای امن (رمزگذاری) هنگام ساخت روشن باشد (بدون آن جواب همیشه "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();
}

صفحه‌ی شروع را انتخاب کنید: گاوصندوق موجود را باز کنید یا یکی بسازید - و داده‌ی مهر و موم‌شده‌ای را که کلیدش رفته پاک کنید:

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

یک کلید برای هر حساب واردشده، ساخته‌شده اولین باری که آن حساب روی این گوشی وارد می‌شود:

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

نکته‌ها: پاسخ یک رشته است، پس if (WebToApk.hasKey('vault')) همیشه درست است - با === 'true' مقایسه کنید. فقط کلیدهایی را می‌بینید که این اپلیکیشن از طریق پل ساخته؛ کلیدهای خود پوسته پنهان‌اند.

isBiometricAvailable پل#

window.WebToApk.isBiometricAvailable(): String

«available» | «no_hardware» | «not_enrolled» | «unavailable». Android 10+ چهره، عنبیه و اثر انگشت را حساب می‌کند؛ Android 6-9 فقط حسگر اثر انگشت را. «unavailable» = حسگر موقتاً غیرقابل‌استفاده است یا بررسی اجرا نشد (مثلاً ساخت مجوز Fingerprint را فعال نکرد). [BiometricStatus] را ببینید.

نمونه

به شما می‌گوید گوشی همین حالا می‌تواند قفل بیومتریک (چهره، عنبیه یا اثر انگشت) انجام دهد یا نه. قبل از نشان دادن دکمه‌ی باز کردن قفل آن را بررسی کنید.

مقدار بازگشتی: یک رشته، بلافاصله: "available"، "no_hardware" (بدون حسگر)، "not_enrolled" (حسگر هست اما چیزی ثبت نشده، یا قفل صفحه تنظیم نشده) یا "unavailable" (حسگر موقتاً غیرقابل‌استفاده است یا بررسی اجرا نشد - مثلاً ساخت Fingerprint را تیک نزده بود). نیازمندی: Fingerprint هنگام ساخت تیک خورده باشد - بدون آن Android از جواب دادن خودداری می‌کند و نتیجه است "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
  }
}

یک کلید تنظیمات «باز کردن قفل با اثر انگشت» که وقتی کار نمی‌کند با ذکر دلیل خاکستری می‌شود:

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

وقتی کاربر از تنظیمات گوشی برمی‌گردد دوباره بررسی کنید، شاید همین الان اثر انگشت اضافه کرده باشد:

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

نکته‌ها: در Android 10 و جدیدتر بررسی چهره، عنبیه و اثر انگشت را حساب می‌کند، پس گوشی‌ای که فقط باز کردن با چهره دارد "available" جواب می‌دهد. در Android 6-9 فقط حسگر اثر انگشت را نگاه می‌کند، پس گوشی فقط‌چهره آنجا "no_hardware" جواب می‌دهد؛ با allowDeviceCredential:true در authenticateBiometricEx کاربر همچنان می‌تواند با پین، الگو یا رمز گوشی قفل را باز کند (Android 11+).

isKeyHardwareBacked پل#

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

درست است وقتی [alias] در TEE یا یک عنصر امن زندگی می‌کند، نه در نرم‌افزار. قبل از اینکه به کاربر قول بدهید نابود کردن یک کلید بازگشت‌ناپذیر است ارزش بررسی دارد.

نمونه

مشخص می‌کند که آیا کلید داخل سخت‌افزار امن گوشی (تراشه‌ی TEE یا StrongBox) قرار دارد یا در نرم‌افزار. بیشتر صفحه‌ها از AppMintKeystore.isHardwareBacked(alias) استفاده می‌کنند که یک مقدار بولین واقعی برمی‌گرداند.

بازگشتی: رشته‌ی "true" یا "false"، بلافاصله. اگر کلید وجود نداشته باشد هم "false". نیازمند: روشن بودن کلیدهای امن (رمزنگاری) هنگام ساخت.

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

از نگه داشتن یک راز باارزش روی گوشی‌ای که فقط می‌تواند کلید را نرم‌افزاری نگه دارد خودداری کنید:

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

یک خلاصه‌ی کوچک از امنیت دستگاه را همراه ورود به سرور خود بفرستید تا بتواند تصمیم بگیرد چه چیزی را مجاز کند:

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

نکته‌ها: در رابط کاربری فقط وقتی «سخت‌افزاری» بگویید که این "true".

isKeystoreEnabled پل#

window.WebToApk.isKeystoreEnabled(): String

اینکه آیا سازنده پل keystore را برای این اپلیکیشن فعال کرده است یا نه.

نمونه

به شما می‌گوید سازنده پل Keystore را برای این اپلیکیشن روشن کرده یا نه. بیشتر صفحه‌ها از AppMintKeystore.isAvailable() استفاده می‌کنند که یک boolean واقعی برمی‌گرداند.

مقدار بازگشتی: رشته‌ی "true" یا "false"، بلافاصله. نیازمندی: برای صدا زدنش چیزی لازم نیست؛ فقط وقتی "true" جواب می‌دهد که کلیدهای امن (رمزگذاری) هنگام ساخت روشن شده باشد.

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

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

«مرا به خاطر بسپار» را فقط وقتی ارائه دهید که توکن بتواند رمزگذاری‌شده ذخیره شود:

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

هنگام آزمایش، «باز شده در مرورگر» را از «اپلیکیشن بدون کلیدهای امن ساخته شده» تشخیص دهید:

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

نکته‌ها: وقتی "false" است، هر متد کلید دیگر "false"، "[]" یا {ok:false, error:"disabled"} جواب می‌دهد. ویژگی را پنهان کنید به‌جای ارائه‌ی دکمه‌ای که نمی‌تواند کار کند.

listKeys پل#

window.WebToApk.listKeys(): String

آرایه‌ی JSON از هر نام مستعار کلیدی که این صفحه ساخته است.

نمونه

نام‌های مستعار هر کلید Keystore را که صفحه‌های این اپلیکیشن ساخته‌اند فهرست می‌کند. بیشتر صفحه‌ها از AppMintKeystore.listKeys() استفاده می‌کنند که آرایه‌ی تجزیه‌شده را برمی‌گرداند.

بازگشتی: یک رشته‌ی آرایه‌ی JSON، مرتب‌شده بر اساس نام، بلافاصله: '["notes","vault"]'. نیازمند: روشن بودن کلیدهای امن (رمزنگاری) هنگام ساخت (بدون آن: "[]").

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

یک صفحه‌ی تنظیمات «کلیدهای امنیتی» که هر کلید، محل آن و یک دکمه‌ی «حذف» را فهرست می‌کند:

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

هنگام خروج از حساب، همه‌ی کلیدهای هر حساب (با نام user_...) را نابود کنید تا هیچ چیزِ ذخیره‌شده روی گوشی دیگر قابل خواندن نباشد:

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

نکته‌ها: فقط کلیدهایی که از طریق پل ساخته شده‌اند فهرست می‌شوند؛ کلیدهای داخلی خود اپلیکیشن هرگز نشان داده نمی‌شوند. همچنین وقتی از صفحه‌ای فراخوانی شود که صفحه‌ی خود اپلیکیشن نیست (سایت دیگری که داخل اپلیکیشن باز شده)، پاسخ "[]" می‌دهد.