دليل تعليمي للتشفير من جانب العميل: ابنِه من الصفر
دليل تعليمي خطوة بخطوة لتنفيذ التشفير من جانب العميل. شفّر الملفات في المتصفح قبل أن تغادر جهاز المستخدم.
تشفير الملفات من جانب العميل في المتصفح يحتاج نحو 80 سطراً من JavaScript باستخدام Web Crypto API. النمط: ولّد مفتاح AES-256-GCM في المتصفح، وشفّر الملف بمُنبَّه عشوائي بـ 96 بت، وارفع النص المشفَّر عبر HTTPS/TLS 1.3، وشارك عنوان URL الناتج بالمفتاح مضمَّناً في معرّف المقطع (#key=...) الذي لا تنقله المتصفحات إلى الخوادم. يفكّ المستلم التشفير في المتصفح باستخدام المقطع ذاته. يستعرض هذا الدليل تنفيذاً عاملاً يشمل التجزئة للملفات الكبيرة والمفاتيح المشتقة من كلمة المرور عبر PBKDF2 بـ 600,000 تكرار والمزالق التي تُعثر المحاولات الأولى.
البنية في مخطط واحد
[متصفح المرسل] [الخادم] [متصفح المستلم]
قراءة الملف → مفتاح AES (عشوائي) يقبل POST GET نص مشفَّر
تشفير بـ AES-256-GCM يخزّن كتلة النص المشفَّر تحليل المفتاح من مقطع URL
POST النص المشفَّر لا مفتاح، لا نص صريح فكّ التشفير في المتصفح
بناء URL بـ #key=... يُعيد URL التنزيل حفظ الملف على القرص
الخادم مخزن بيانات غبي. لا يرى سوى النص المشفَّر ولا يستطيع فكّه. يعيش مفتاح التشفير في مقطع URL الذي تعامله المتصفحات بصورة خاصة: لا يُرسَل في سطر طلب HTTP. هذا أساس كل خدمة نقل ملفات بمعرفة صفرية بما فيها HexaTransfer.
الخطوة 1: توليد مفتاح متماثل
async function generateKey() {
return await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // قابل للاستخراج لنتمكن من تصديره في URL
["encrypt", "decrypt"]
);
}
علامة extractable: true مطلوبة لأننا نحتاج تسلسل المفتاح في مقطع URL. إذا كنت تبني تدفقاً يعيش فيه المفتاح في الذاكرة فقط، اضبطها على false.
الخطوة 2: قراءة الملف كـ ArrayBuffer
async function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
هذا يُحمّل الملف كاملاً في الذاكرة. مناسب للملفات دون 500 ميجابايت. للملفات الأكبر، انتقل إلى قسم البث.
الخطوة 3: تشفير المخزن المؤقت
async function encryptFile(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
// أُلحق المُنبَّه بالنص المشفَّر ليتمكن المستلم من استخراجه
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
return combined.buffer;
}
المُنبَّه (IV) بـ 96 بت (12 بايت) وفق NIST SP 800-38D. ليس سراً لكن يجب أن يكون فريداً لكل مفتاح. المُنبَّهات العشوائية آمنة هنا لأننا نولّد مفتاحاً جديداً لكل ملف. إلحاق المُنبَّه بالنص المشفَّر اصطلاح شائع؛ يفصله المستلم قبل فكّ التشفير.
الخطوة 4: رفع النص المشفَّر
async function uploadCiphertext(ciphertext) {
const response = await fetch("/api/upload", {
method: "POST",
body: ciphertext,
headers: { "Content-Type": "application/octet-stream" },
});
const { fileId } = await response.json();
return fileId;
}
يتلقى الخادم كتلة ثنائية، يُسنِد لها معرِّفاً، يخزّنها، ويُعيد ذلك المعرِّف. لا رؤوس تكشف اسم الملف، لا معاملات استعلام تحمل المفتاح. إذا سُرق قرص الخادم غداً، يرى المهاجم هراءً.
الخطوة 5: بناء URL المشاركة بالمفتاح في المقطع
async function buildShareURL(fileId, key) {
const rawKey = await crypto.subtle.exportKey("raw", key);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return `${location.origin}/f/${fileId}#${keyBase64}`;
}
ترميز base64url (مع - و_ بدلاً من + و/) يتجنب مشاكل ترميز URL. يُجرَّد حشو = للجمالية.
المقطع (#...) هو السحر هنا. حين يُحمَّل المستلم URL، يُبقي المتصفح المقطع من جانب العميل. طلب GET لـ /f/{fileId} لا يتضمن #keyBase64 في سطر الطلب، لذا الخادم لا يعلم المفتاح قط. تحقق من هذا بنفسك بفتح أدوات المطوّر في المتصفح على أي URL يحتوي مقطعاً ومشاهدة علامة تبويب الشبكة.
الخطوة 6: فكّ التشفير من جانب المستلم
async function downloadAndDecrypt() {
const fileId = location.pathname.split("/").pop();
const keyBase64 = location.hash.slice(1);
const rawKey = Uint8Array.from(
atob(keyBase64.replace(/-/g, "+").replace(/_/g, "/")),
c => c.charCodeAt(0)
);
const key = await crypto.subtle.importKey(
"raw", rawKey, "AES-GCM", false, ["decrypt"]
);
const response = await fetch(`/api/download/${fileId}`);
const combined = new Uint8Array(await response.arrayBuffer());
const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv }, key, ciphertext
);
const blob = new Blob([plaintext]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "downloaded-file";
a.click();
}
بطاقة مصادقة GCM تُفحص أثناء decrypt(). إذا عُبِث بالنص المشفَّر، الاستدعاء يرمي OperationError — وضع فشل نظيف.
المفاتيح المشتقة من كلمة المرور عبر PBKDF2
إذا أدخل المستخدمون كلمة مرور بدلاً من مفتاح عشوائي، اشتق مفتاح AES عبر PBKDF2:
async function deriveKey(password, salt) {
const passwordKey = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(password),
"PBKDF2", false, ["deriveKey"]
);
return await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
600,000 تكرار لـ PBKDF2-SHA-256 هو خط أساس OWASP 2023. الملح يجب أن يكون 16 بايت عشوائياً ومخزَّناً مع النص المشفَّر (ليس سراً، بل يجب أن يكون فريداً). للكود الجديد، فكّر في Argon2id عبر مكتبة كـ argon2-browser — يقاوم هجمات GPU بصورة أفضل بكثير.
بث الملفات الكبيرة
الملفات فوق 500 ميجابايت يجب تجزئتها. اقرأ عبر File.stream() وشفّر كل جزء وارفع بالتسلسل:
async function encryptStream(file, key) {
const reader = file.stream().getReader();
const chunks = [];
let chunkIndex = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const iv = new Uint8Array(12);
// رمّز فهرس الجزء في المُنبَّه لضمان التفرد
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, value
);
chunks.push({ iv, ct });
}
return chunks;
}
اشتقاق المُنبَّه من فهرس الجزء يضمن التفرد دون تتبع الحالة. يفكّ المستلم التشفير بالترتيب ويسلسل الأجزاء.
للـ AEAD البثي الحقيقي، crypto_secretstream_xchacha20poly1305 من libsodium عبر libsodium.js أنظف ويكتشف هجمات الاقتطاع. Web Crypto لا يمتلك أولية مكافئة عام 2026.
الاختبار والمزالق
أخطاء شائعة يجب تجنبها:
- استخدام
Math.random()للمفاتيح أو المُنبَّهات: دائماًcrypto.getRandomValues(). - إعادة استخدام مُنبَّه مع المفتاح ذاته: يكسر أمان GCM. المفاتيح العشوائية لكل ملف تجعل هذا آمناً؛ تدفقات الأجزاء تحتاج مُنبَّهات فريدة لكل جزء.
- عدم التحقق من HTTPS:
crypto.subtleغير معرَّفة على الأصول غير الآمنة. اختبر على localhost أو مع شهادة موقَّعة ذاتياً أثناء التطوير. - تخزين المفاتيح في
localStorage: أي XSS على أصلك يستطيع قراءتها. استخدم نمط مقطع URL بدلاً من ذلك، أو مفاتيح غير قابلة للاستخراج. - نسيان تضمين المُنبَّه مع النص المشفَّر: يفشل فكّ التشفير بلا خطأ مفيد. ألحق أو سلسِل دائماً.
- التعامل الخاطئ مع المقطع: لا تنشر URL (بالمقطع) عرضاً إلى خدمة طرف ثالث. شارك فقط عبر قنوات كاملة التشفير إذا كان المقطع حساساً.
مسؤوليات جانب الخادم
عمل الخادم في بنية التشفير من جانب العميل صغير: اقبل POST، خزّن الكتلة، أعِد المعرِّف، اخدم GET للكتلة، احذف عند انتهاء الصلاحية. لا تشفير. ما يجب أن يفعله الخادم فوق التخزين:
- تفعيل حدود حجم الملف (منع الإساءة).
- تحديد معدل الرفع والتنزيل.
- ضبط احتفاظ قصير (7 أيام مناسب، كـ HexaTransfer).
- تسجيل ما يلزم فقط (توقيت الرفع، لا عناوين IP إذا أُعطيت الأولوية للخصوصية).
- الخدمة عبر TLS 1.3 مع HSTS.
- رؤوس CORS تقيّد الأصول إذا استُدعيت الـ API من نطاقاتك فقط.
تجميع كل شيء
تطبيق عامل بسيط يناسب ملف HTML واحد مع خلفية Express بـ 50 سطراً. التبعيات الكلية: لا شيء على جانب العميل (Web Crypto أصلي)، Express وmulter على الخادم. التشفير بمتانة أولية AES-256-GCM لأن هذا هو ما تستخدمه حرفياً. لا خوارزمية سرية يمكن الخطأ فيها، فقط الأوليّات التي يجب استخدامها بصورة صحيحة.
أصعب الأجزاء هي حالات الحافة: الملفات الكبيرة وتدفقات كلمة المرور إلى مفتاح وتجربة المستلم حين يفشل فكّ التشفير والتعامل مع الروابط المنتهية الصلاحية بصورة رشيقة. تشفير النواة مباشر.
جرّبها على hexatransfer.com — مجاني، بدون حساب، حتى 10 جيجابايت.
أرسل ملفات كبيرة بأمان مع تشفير من طرف إلى طرف
انقل ملفات حتى 10 جيجابايت مجاناً مع تشفير من طرف إلى طرف. لا حاجة لحساب. يتم تشفير ملفاتك في متصفحك قبل الرفع — لا أحد آخر يستطيع قراءتها.
إرسال ملف