أنشئ APK مجانًا

واجهة JavaScript bridge API

القياسات الحيوية وKeystore

فتح القفل بالبصمة والوجه، ومفاتيح مدعومة بالعتاد تستطيع الصفحة التشفير بها دون قراءتها أبدًا.

AppMintKeystore.decrypt دالة مساعدة (helper)#

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 دالة مساعدة (helper)#

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

أتلف الخزنة ذاتيًا بعد عدد كبير من أرقام PIN الخاطئة للتطبيق:

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 دالة مساعدة (helper)#

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 دالة مساعدة (helper)#

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

ملاحظات: الاسم المستعار: أحرف وأرقام و_ - .، حتى 64 حرفًا (وإلا error:"bad alias"). استخدام اسم مستعار موجود يستبدل المفتاح، وتضيع البيانات المختومة بالمفتاح القديم. أخطاء أخرى: disabled وbad options json وforeign_origin وunsupported (لا يوجد جسر).

AppMintKeystore.hasKey دالة مساعدة (helper)#

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 دالة مساعدة (helper)#

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 دالة مساعدة (helper)#

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 دالة مساعدة (helper)#

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 الجسر (bridge)#

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 (استخدم تلك لخيار PIN أو تسمية إلغاء مخصصة أو القياسات "القوية").

تُرجع: لا شيء. يصل الرد لاحقًا: يستدعي الغلاف window.__webToApkAuth[callbackId](json) مع '{"ok":true}' أو '{"ok":false,"error":"<code>"}'، ثم يزيل ذلك الإدخال. المتطلبات: تحديد البصمة عند الإنشاء (بدونه: error:"permission_missing").

سجّل دالة الاستدعاء أولًا، ثم استدعِ:

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 الجسر (bridge)#

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

الصيغة الكاملة. يقبل [optionsJson]: title, subtitle, description, negativeButtonText, allowDeviceCredential (بديل PIN/نمط/كلمة مرور)، strong (يتطلب BIOMETRIC_STRONG، استخدمه لأي شيء يخص الدفع). الأخطاء رموز مميزة وليست قيمة منطقية واحدة. قبل أي نافذة: no_hardware | none_enrolled | unavailable (تعذّر فحص الحالة، راجع isBiometricAvailable). من نافذة نظام Android 9+: user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error، ولأي رمز يضيفه Android لاحقًا يُعاد نص رسالته (أو `error_<code>`). permission_missing عندما يفتقر الإنشاء إلى إذن Fingerprint. على Android 7-8 (API 24-27، البصمة فقط) يكون الخطأ نص رسالة الهاتف نفسه كما كتبته الشركة المصنّعة، وليس رمزًا ثابتًا.

مثال

يعرض مطالبة بصمة / وجه النظام بكل الخيارات: PIN أو نمط كطريقة دخول ثانية، وتسمية إلغاء مخصصة، والقياسات "القوية" فقط.

تُرجع: لا شيء. يصل الرد لاحقًا: يستدعي الغلاف window.__webToApkAuth[callbackId](json) مع '{"ok":true}' أو '{"ok":false,"error":"<code>"}'، ثم يزيل ذلك الإدخال. المتطلبات: تحديد البصمة عند الإنشاء (بدونه: 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));
  });
}

افتح قفل التطبيق - بصمة أو وجه أو PIN/نمط الهاتف:

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 (يجب أن يفتح المستخدم الهاتف بـ PIN أولًا)، no_device_credential، timeout، permission_missing، unavailable، ونادرًا no_space، unable_to_process، vendor_error. على Android 7-8 يعرض الغلاف مربع حوار خاصًا به والخطأ هو نص رسالة النظام. لا يجتمع allowDeviceCredential وnegativeButtonText: مع خيار PIN يرسم النظام زره الخاص. الإصبع الخاطئ لا ينهي المطالبة.

decryptWithKey الجسر (bridge)#

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:

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 الجسر (bridge)#

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

يتلف [alias] نهائيًا. كل ما شُفّر به يصبح غير قابل لفك التشفير، حتى بواسطة هذا التطبيق أو أي شخص يحمل الجهاز.

مثال

يُتلف مفتاح Keystore نهائيًا. كل ما شُفّر به لن يمكن قراءته مجددًا - لا منك ولا من المستخدم ولا من أي شخص يحمل الهاتف. تستخدم معظم الصفحات AppMintKeystore.deleteKey(alias)، الذي يُرجع قيمة منطقية حقيقية.

يُرجع: النص "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 الجسر (bridge)#

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

شفّر صورة يختارها المستخدم (بيانات ثنائية، حتى 4 MB لكل استدعاء):

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 جديدًا، لذا يعطي النص نفسه نتيجة مختلفة في كل مرة. بحد أقصى 4 MB لكل استدعاء (error:"chunk too large ...")؛ للملفات الأكبر شفّر قطعةً قطعة، أو استخدم WebToApkFS.writeSealed. رموز الأخطاء: no-such-key وauth-required وkey-invalidated (غيّر المستخدم قفل الشاشة، فالمفتاح ضاع نهائيًا)، bad base64, disabled, foreign_origin.

generateKey الجسر (bridge)#

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 الجسر (bridge)#

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 الجسر (bridge)#

window.WebToApk.isBiometricAvailable(): String

"available" | "no_hardware" | "not_enrolled" | "unavailable". يحسب Android 10+ الوجه والقزحية والبصمة؛ وAndroid 6-9 مستشعر البصمة فقط. "unavailable" = المستشعر غير صالح مؤقتًا، أو تعذّر إجراء الفحص (مثلًا لم يفعّل البناء إذن البصمة). راجع [BiometricStatus].

مثال

يخبرك هل يستطيع الهاتف فتح القفل بالقياسات الحيوية (الوجه أو القزحية أو البصمة) الآن. تحقق منه قبل عرض زر فتح القفل.

تُرجع: سلسلة، فورًا: "available"، "no_hardware" (لا مستشعر)، "not_enrolled" (مستشعر لكن لا شيء مسجّل، أو لا قفل شاشة) أو "unavailable" (المستشعر غير صالح مؤقتًا، أو تعذّر الفحص - مثلًا لم يحدد البناء البصمة). المتطلبات: تحديد البصمة عند الإنشاء - بدونه يرفض 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 يستطيع المستخدم مع ذلك فتح القفل بـ PIN الهاتف أو نمطه أو كلمة مروره (Android 11+).

isKeyHardwareBacked الجسر (bridge)#

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 الجسر (bridge)#

window.WebToApk.isKeystoreEnabled(): String

ما إذا كان المنشئ قد فعّل جسر keystore لهذا التطبيق.

مثال

يخبرك هل فعّل المنشئ جسر Keystore لهذا التطبيق. تستخدم معظم الصفحات AppMintKeystore.isAvailable()، الذي يُرجع قيمة منطقية حقيقية.

تُرجع: السلسلة "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 الجسر (bridge)#

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

ملاحظات: تُسرد فقط المفاتيح التي أُنشئت عبر الجسر؛ أما المفاتيح الداخلية للتطبيق نفسه فلا تظهر أبدًا. كما يُرجع "[]" عند استدعائه من صفحة ليست من صفحات التطبيق نفسه (موقع آخر محمّل داخل التطبيق).

يتم إنشاؤه من بيئة تشغيل التطبيق وملفات الأمثلة الخاصة به عند كل بناء للمستندات. اقرأه بصيغة Markdown · جميع العائلات.

تمت المراجعة مقابل الجسر المُرفق بتاريخ 2026-10-22.