برنامه نویسی

چگونه پیام رسان ها در واقع پیام ها را رمزگذاری می کنند (پایان به پایان)

سلام به همه!

من مقاله ای نوشتم و زودرس آن را منتشر می کنم. در ابتدا ، من قصد داشتم پس از اتمام پروژه ، آن را بنویسم ، اما از آنجا که هنوز دو ماه تا پایان باقی مانده است ، تصمیم گرفتم وقت خود را تلف نکنم و مقاله را بنویسم در حالی که اطلاعات هنوز در ذهن من تازه است. علاوه بر این ، من بیشتر این را برای خودم می نویسم. 🙂 در یکی از آخرین پروژه های من ، که من به عنوان منبع باز در حال توسعه هستم ، رمزگذاری پایان به پایان را اجرا کردم-به عنوان مثال ، به عنوان مثال WhatsApp یا Telegram این کار را انجام می دهد.

در این مقاله ، ما به اجرای رمزگذاری پیام سمت مشتری با استفاده از JavaScript و وب Crypto API خواهیم رسید و نمونه ای عملی را که در انتهای مقاله خواهد بود ، تجزیه می کنیم.

بیایید با بیان اینکه اگر شما یک مبتدی کامل در رمزنگاری هستید ، درک آنچه در اینجا نوشته شده است ممکن است آسان نباشد. حتی با 10 سال تجربه توسعه ، مجبور شدم سرم را کمی خراش دهم – هر چیزی که در اینجا اتفاق می افتد ریاضیات خالص است ، که در این مقاله در مورد آن بحث نخواهیم کرد 🙂 که به راحتی تحت تأثیر قرار می گیرد ممکن است فکر کند جادویی است 🙂

اگر بخواهم به طور خلاصه جوهر رمزگذاری پایان به پایان را بدون کلمات و اصطلاحات پیچیده توضیح دهم:

جادوی رمزگذاری در سه کلید

شرح تصویر
سه کلید پایه ای است که رمزگذاری پایان به پایان بر اساس آن ساخته شده است. به کلید موجود در مرکز توجه کنید – این مهم است.

پایه ای که همه چیز در آن قرار دارد این سه کلید است. اگر چیزی نامشخص است به این بخش مراجعه کنید.

  1. کلید خصوصی: ذخیره شده (به صورت رمزگذاری شده).
  2. کلید عمومی: برای همه قابل دسترسی است.
  3. کلید مخفی / متقارن مشترک: بر اساس کلید خصوصی شما + کلید عمومی مخاطب شما تولید می شود. این کلید مورد استفاده برای رمزگذاری مستقیم و رمزگشایی پیام ها است.

ترکیبی از کلید خصوصی شما + کلید عمومی مخاطب شما به شما امکان می دهد کلید مخفی مشترک را بدست آورید (در مثال زیر ، این یک کلید AES خواهد بود). با تشکر از این کلید مخفی مشترک ، می توانید پیام ها را رمزگذاری و رمزگشایی کنید.

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

کلید مخفی مشترک (موردی که به آن گفته می شود. Aeeskey در کد زیر) در پایگاه داده ذخیره نمی شود. هر بار که یک گپ با یک تماس خاص شروع می شود (محاسبه می شود) تولید می شود. این ممکن است باعث سردرگمی شود: اگر این کلید ذخیره نشده باشد اما دوباره تولید می شود ، چگونه پیام ها را رمزگشایی می کنیم؟ این جایی است که “جادوی” رمزگذاری نامتقارن و پروتکل های تبادل کلید نهفته است.

کلید مشترک “کلید خصوصی شما” + “کلید عمومی مخاطب” است.

هنگامی که با یک مخاطب گپ باز می کنید ، مشتری شما این کلید مخفی مشترک را دوباره محاسبه می کند ، همانطور که از قبل می دانید ، مانند این: (کلید خصوصی شما + کلید عمومی مخاطب شما = کلید مخفی مشترک). با استفاده از این کلید ، شما پیام های جدید را رمزگذاری می کنید و تمام پیام های قبلی را در این گپ رمزگذاری می کنید ، زیرا آنها با همان کلید مخفی مشترک رمزگذاری شده اند. تا زمانی که رمزگذاری نامتقارن و پروتکل های تبادل کلیدی را درک نکنید ، می توانید مغز خود را برای مدت طولانی بچسبانید.

رمزگذاری نامتقارن و ECDH

رمزگذاری نامتقارن از یک جفت کلید استفاده می کند: عمومی و خصوصی. کلید عمومی را می توان آزادانه توزیع کرد ، در حالی که کلید خصوصی باید توسط صاحب آن مخفی نگه داشته شود ، به این معنی که به صورت رمزگذاری شده.

ECDH (منحنی بیضوی Diffie-Hellman) یک پروتکل تبادل کلیدی مبتنی بر ریاضیات منحنی بیضوی است. این اجازه می دهد تا دو حزب که هر یک از آنها دارای جفت کلید ECDH (خصوصی و عمومی) هستند ، یک کلید مخفی مشترک را بر روی یک کانال ناامن ایجاد کنند. مهمتر اینکه ، شخص ثالث ، حتی اگر کلیدهای عمومی خود را رهگیری کند ، نمی تواند این راز مشترک را محاسبه کند. مثال ما از منحنی P-256 استفاده می کند-یک استاندارد محبوب و قابل اعتماد.

من فکر می کنم کمتر کسی فهمید که آنها فقط می خوانند. تمام آنچه شما باید در این مرحله درک کنید این است که این فناوری کار می کند 🙂 بعداً ، این معما با هم جمع می شود ، شاید پس از یک بازخوانی. و اکنون ، کمی در مورد فن آوری های مرورگر داخلی.

وب Crypto API

وب Crypto API یک رابط JavaScript است که در مرورگرهایی ساخته شده است که دسترسی به بدوی های رمزنگاری سطح پایین را فراهم می کند. این امکان را برای انجام عملیاتی مانند هشویی ، تولید امضا ، رمزگذاری و رمزگشایی فراهم می کند. استفاده از API Crypto Web برای کتابخانه های شخص ثالث برای عملیات اساسی رمزنگاری ارجح است ، زیرا اغلب برای امنیت شتاب سخت و کاملاً مورد بررسی قرار می گیرد. تمام عملیات API Crypto Web ناهمزمان هستند و یک وعده را برمی گردانند.

حال بیایید به سمت تحلیل عملی حرکت کنیم. من یک کلاس chatcrypto ایجاد کرده ام ، که با جزئیات بیشتری بررسی خواهیم کرد:

class ChatCrypto {

  constructor(myPrivateKeyBase64, theirPublicKeyBase64) {
    this.myPrivateKeyBase64 = myPrivateKeyBase64;
    this.theirPublicKeyBase64 = theirPublicKeyBase64;
    this.aesKey = null; // The shared symmetric AES key will be stored here
  }

  static base64ToArrayBuffer(base64) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  }

  static arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let b of bytes) {
      binary += String.fromCharCode(b);
    }
    return btoa(binary);
  }

  init() {

    // Convert keys from Base64 to ArrayBuffer
    const privateRaw = ChatCrypto.base64ToArrayBuffer(this.myPrivateKeyBase64);
    const publicRaw = ChatCrypto.base64ToArrayBuffer(this.theirPublicKeyBase64);

    // Note: The following lines for parsing publicRaw into x and y coordinates,
    // and assembling uncompressedPoint might be specific to a particular format
    // for representing a "raw" public key. If publicRaw is already in SPKI format,
    // they might not be necessary, as crypto.subtle.importKey("spki", ...)
    // expects a standard structure.
    // const x = publicRaw.slice(0, publicRaw.byteLength / 2);
    // const y = publicRaw.slice(publicRaw.byteLength / 2);
    // const uncompressedPoint = new Uint8Array([0x04, ...new Uint8Array(x), ...new Uint8Array(y)]);

    // Import our private key
    return crypto.subtle.importKey(
      "pkcs8", // Private key format (standard)
      privateRaw,
      { name: "ECDH", namedCurve: "P-256" }, // Algorithm and parameters
      false, // Non-exportable
      ["deriveBits"] // Allowed usage: for deriving bits (shared secret)
    ).then(privateKey => {
      // Import the contact's public key
      return crypto.subtle.importKey(
        "spki", // Public key format (standard)
        publicRaw,
        { name: "ECDH", namedCurve: "P-256" },
        false, // Non-exportable
        [] // Specific uses are not needed here for the public key in ECDH
      ).then(publicKey => {
        // 4. Compute the shared secret (deriveBits)
        return crypto.subtle.deriveBits(
          { name: "ECDH", public: publicKey }, // Specify the contact's public key
          privateKey, // Our private key
          256 // Length of the derived secret in bits
        );
      });
    }).then(sharedBits => {
      // Hash the shared secret to obtain an AES key (using SHA-256 as KDF)
      return crypto.subtle.digest("SHA-256", sharedBits);
    }).then(hashed => {
      // Import the hashed secret as an AES-GCM key
      return crypto.subtle.importKey(
        "raw", // "Raw" byte format
        hashed, // Hashed secret
        { name: "AES-GCM" }, // Symmetric encryption algorithm
        false, // Non-exportable
        ["encrypt", "decrypt"] // Allowed uses: encryption and decryption
      );
    }).then(aesKey => {
      this.aesKey = aesKey; // Save the obtained AES key
      return true;         // Signal successful initialization
    });
  }

  encrypt(plaintext) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Generate a unique initialization vector (IV)
    const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 bytes (96 bits) is recommended for AES-GCM

    // Convert the text message to bytes (UTF-8)
    const encoded = new TextEncoder().encode(plaintext);

    // Encrypt the data
    return crypto.subtle.encrypt(
      { name: "AES-GCM", iv: iv }, // Algorithm and IV
      this.aesKey, // Our shared AES key
      encoded // Data to encrypt
    ).then(encrypted => {
      // 4. Return IV and encrypted data (in Base64 for convenient transmission)
      return {
        iv: ChatCrypto.arrayBufferToBase64(iv),
        data: ChatCrypto.arrayBufferToBase64(encrypted)
      };
    });
  }

  decrypt(cipherBase64, ivBase64) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Convert ciphertext and IV from Base64 to ArrayBuffer
    const encrypted = ChatCrypto.base64ToArrayBuffer(cipherBase64);
    const ivBuffer = ChatCrypto.base64ToArrayBuffer(ivBase64);

    // Decrypt the data
    return crypto.subtle.decrypt(
      { name: "AES-GCM", iv: new Uint8Array(ivBuffer) }, // Algorithm and IV (must be a TypedArray)
      this.aesKey, // The same shared AES key
      encrypted // Encrypted data
    ).then(decrypted => {
      // Convert decrypted bytes back to a string
      return new TextDecoder().decode(decrypted);
    });
  }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

سازنده () و base64toarraybuffer () روش:
سازنده کلید خصوصی شما و کلید عمومی مخاطب را با فرمت Base64 می پذیرد. Base64 راهی برای رمزگذاری داده های باینری در یک رشته متنی است که برای انتقال یا ذخیره سازی مناسب است.

این. AESKEY به عنوان تهی آغاز می شود و پس از اتمام موفقیت آمیز () با موفقیت جمع می شود.

روشهای استاتیک Base64toArrayBuffer و ArrayBuffertoBase64 برای تبدیل داده ها بین رشته های Base64 و ArrayBuffer (Format Web Crypto API با آن کار می کند) استفاده می شود.

روش INIT (): ایجاد کلید AES مشترک
این قلب کلاس ما است ، جایی که “جادوی” ECDH رخ می دهد و کلید مشترک برای رمزگذاری متقارن ایجاد می شود.

تجزیه مراحل در init ():

تبدیل کلیدی: کلیدها از Base64 به Arraybuffer تبدیل می شوند.
وارد کردن کلید خصوصی: کلید خصوصی شما با فرمت PKCS8 وارد می شود. مشخص شده است که این یک کلید ECDH در منحنی P-256 است و برای مشتق ها (محاسبه راز مشترک) استفاده می شود.

کلید عمومی Import Contact: کلید عمومی مخاطب با فرمت SPKI وارد می شود.

محاسبه مشترک راز (مشتق): این مرحله اصلی ECDH است. DeriveBits با استفاده از کلید خصوصی و کلید عمومی مخاطب ، مجموعه ای مخفی مشترک از بیت ها (مشترک) را محاسبه می کند. اگر آنها از کلیدهای خصوصی مربوطه و کلیدهای عمومی یکدیگر استفاده کنند ، این راز برای شما و تماس شما یکسان خواهد بود.

هش مشترک راز (DIGEST): مشترک با استفاده از SHA-256 هشدار داده می شود. این یک روش معمول برای تبدیل خروجی مشتقات به یک کلید رمزنگاری قوی از طول مورد نظر برای یک رمز متقارن (AE در این مورد) است. این مرحله همچنین به عنوان KDF (عملکرد مشتق کلید) عمل می کند.

Import AES Key (ImportKey): هش حاصل (هشده) به عنوان کلید “خام” برای الگوریتم AES-GCM وارد می شود. این کلید (this.aeskey) اکنون آماده است تا برای رمزگذاری و رمزگشایی پیام ها استفاده شود.
پس از اجرای موفقیت آمیز ، این.Aeskey حاوی یک شی Cryptokey است که برای استفاده آماده است.

روش رمزگذاری (متن ساده): رمزگذاری یک پیام
تجزیه مراحل در رمزگذاری ():

تولید IV (بردار اولیه سازی): یک IV تصادفی 12 بایت ایجاد می شود. به عنوان یک یادآوری ، باید برای هر رمزگذاری با همان کلید منحصر به فرد باشد.

رمزگذاری متن: پیام از یک رشته JavaScript به uint8array (دنباله ای از بایت در رمزگذاری UTF-8) با استفاده از TextEncoder تبدیل می شود.
رمزگذاری: Crypto.Suble.Encrypt رمزگذاری داده ها را با استفاده از AES-GCM ، این.aeskey ما و IV تولید شده انجام می دهد.

نتیجه بازگشت: داده های رمزگذاری شده و IV (هر دو در Base64) به عنوان یک شی بازگردانده می شوند. IV باید همراه با متن رمزگذار به گیرنده منتقل شود ، زیرا برای رمزگشایی لازم است.
رمزگشایی (cipherbase64 ، ivbase64) روش: رمزگشایی یک پیام
تجزیه مراحل در رمزگشایی ():

تبدیل داده ها: متن رمزگذاری شده و IV (در Base64) به Arraybuffer تبدیل می شوند. توجه داشته باشید که برای crypto.subtle.decrypt ، پارامتر IV باید یک TypedArray باشد (به عنوان مثال ، Uint8Array) ، بنابراین ما Uint8Array جدید (IVBuffer) را پشت سر می گذاریم.

رمزگشایی: crypto.subtle.decrypt رمزگشایی را انجام می دهد. نکته مهم ، AES-GCM نه تنها داده ها را رمزگشایی می کند بلکه یکپارچگی و اصالت آن را نیز تأیید می کند ، با استفاده از همان Aeskey و IV که برای رمزگذاری استفاده شده است. اگر داده ها با آن دستکاری شده باشند ، کلید اشتباه است ، یا IV اشتباه است ، روش رمزگشایی خطایی را برمی گرداند (قول را رد کنید).

رمزگشایی متن: بایت های رمزگشایی با موفقیت با استفاده از TextDecoder به یک رشته قابل خواندن تبدیل می شوند.

  1. چگونه با هم کار می کند: نسل جفت کلید جریان مفهومی:

کاربر A جفت کلید ECDH خود را (PKA عمومی و SKA خصوصی) تولید می کند.
کاربر B همین کار را می کند (PKB عمومی و SKB خصوصی). این مرحله در کد ارائه شده ChatCrypto نشان داده نشده است ، اما در زیر در بخش مثالها نشان داده شده است (برای مثال در هنگام ثبت نام کاربر یک بار انجام می شود) ، و پیش از استفاده از کلاس است. Web Crypto API برای این روش Crypto.Suble.GenerateKey را دارد. کلید خصوصی SK باید ایمن ذخیره شود و با رمز عبور کاربر رمزگذاری شود.
مبادله کلید عمومی:

کاربر A PKA کلید عمومی خود را به کاربر B منتقل می کند.
کاربر B PKB کلید عمومی خود را به کاربر منتقل می کند. این مبادله باید ایمن باشد تا از حملات میانه (MITM) جلوگیری شود. به عنوان مثال ، از طریق یک سرور ایمن یا با تأیید اثر انگشت کلید.

chatcrypto اولیه سازی و محاسبات کلیدی مخفی مشترک:

از طرف کاربر A: Const Cryptoa = chatCrypto جدید (SKA_BASE64 ، PKB_BASE64) ؛ منتظر cryptoa.init () ؛
از طرف کاربر B: const Cryptob = chatCrypto جدید (SKB_BASE64 ، PKA_BASE64) ؛ منتظر cryptob.init () ؛ در نتیجه ، هر دو (cryptoa.aeskey و cryptob.aeskey) همان کلید AES متقارن را محاسبه می کنند.
تبادل پیام:

کاربر یک پیام را برای b رمزگذاری می کند: const {iv ، data} = منتظر chatcryptoa.encrypt (“سلام ، b!”) ؛ سپس A {IV ، Data} را به کاربر B ارسال می کند.

کاربر B {IV ، Data} و رمزگشایی را دریافت می کند: پیام const = منتظر chatcryptob.decrypt (داده ، IV) ؛ // پیام “سلام ، ب!” خواهد بود

مثال: رمزگذاری رمزگذاری متن ()

// Initialize the class
let chatCrypto = new ChatCrypto( "My_private_key" , "Contact_public_key" );

// Run it
chatCrypto.init().then(() => {
   chatCrypto.encrypt("Text").then(result => {

     // The encrypted text will be output
      console.log(result);

  });
});
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

مثال: رمزگشایی متن رمزگشایی ()

// Initialize the class
let chatCrypto = new ChatCrypto( "My_private_key" , "Contact_public_key" );

// Run it
chatCrypto.init()
  .then( () => chatCrypto.decrypt("Encrypted_text", "Vector_key_iv") ) // Note: "Vector_key" likely means IV
  .then(result => {

    // The decrypted text will be output
    console.log(result);

  })
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پایان

ما بررسی کرده ایم که چگونه می توان رمزگذاری پیام نهایی به پایان به پایان در JavaScript را با استفاده از API Web Crypto پیاده سازی کرد. ترکیبی از ECDH برای تبادل کلید ایمن و AES-GCM برای رمزگذاری داده های کارآمد و معتبر یک رویکرد قدرتمند و مدرن است. کلاس ChatCrypto به عنوان یک نمونه خوب از چنین اجرای است. اهمیت تولید امن ، ذخیره کلیدهای خصوصی و تبادل قابل اعتماد کلیدهای عمومی را برای ساخت یک سیستم امن به خاطر بسپارید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا