typescript - WebCrypto API kullanarak, stringden oluşturulan şifreleme anahtarı ile verileri şifreleme/şifre çözme


Web uygulamamda, kullanıcı uygulamadan çıkış yaptığında verileri yerel depolamaya kaydetmeye ve tekrar giriş yaptıktan sonra geri yüklemeye çalışıyorum. Bu veriler özeldir, bu yüzden kaydetmeden önce şifrelenmeleri gerekir. Bu gereklilik nedeniyle, prosedür şu şekilde görünmektedir:

Şifreleme:

  1. Backend'den benzersiz dize (anahtar) talep edin (geçerli kullanıcı adı ve tarih-saat parametrelerdir).
  2. window.crypto.subtle.importKey() kullanarak bu dizgeden AES-GCM şifreleme anahtarı oluştur
  3. Veriyi şifreleyin ve yerel depolamaya koyun (anahtarı backend'den almak için kullanılan başlatma vektörü ve tarih-saat ile birlikte).

Şifre çözme:

  1. Kullanıcı tekrar giriş yapana kadar bekleyin.
  2. Backend'den benzersiz dize (anahtar) talep edin (geçerli kullanıcı adı ve tarih-saat parametrelerdir).
  3. window.crypto.subtle.importKey() kullanarak bu dizgeden AES-GCM şifreleme anahtarı oluştur
  4. Verileri yerel depolamadan al ve çöz.

İşte kod (TypeScript):

interface Data {
  queue: string;
  initializationVector: string;
  date: string;
}

private getEncryptionKey(): void {
  const date: string = this.getDateParamForEncryptionKeyGeneration();
  const params = new HttpParams().set('date', date);
  this.encryptionKeyDate = DateSerializer.deserialize(date);
  this.http.get(this.ENCRYPTION_KEY_ENDPOINT, {params}).subscribe((response: {key: string}) => {
    const seed = response.key.slice(0, 32);
    window.crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(seed),
      'AES-GCM',
      true,
      ['encrypt', 'decrypt']
    ).then(
      (key: CryptoKey) => {
        this.encryptionKey = key;
        this.decrypt();
      }
    );
  });
}

private getDateParamForEncryptionKeyGeneration(): string {
  const dataAsString: string = this.localStorageService.getItem(...);
  const data: Data = dataAsString ? JSON.parse(dataAsString) : null;
  return data ? data.date : DateSerializer.serialize(moment());
}

private decrypt(data: Data): void {
  const encoder = new TextEncoder();
  const encryptionAlgorithm: AesGcmParams = {
    name: 'AES-GCM',
    iv: encoder.encode(data.initializationVector)
  };
  window.crypto.subtle.decrypt(
    encryptionAlgorithm,
    this.encryptionKey,
    encoder.encode(data.queue)
  ).then(
    (decryptedData: ArrayBuffer) => {
      const decoder = new TextDecoder();
      console.log(JSON.parse(decoder.decode(decryptedData)));
    }
  );
}

private encrypt(queue: any[]): void {
  const initializationVector: Uint8Array = window.crypto.getRandomValues(new Uint8Array(12));
  const encryptionAlgorithm: AesGcmParams = {
    name: 'AES-GCM',
    iv: initializationVector
  };
  window.crypto.subtle.encrypt(
    encryptionAlgorithm,
    this.encryptionKey,
    new TextEncoder().encode(JSON.stringify(queue))
  ).then((encryptedQueue: ArrayBuffer) => {
    const decoder = new TextDecoder();
    const newState: Data = {
      queue: decoder.decode(encryptedQueue),
      initializationVector: decoder.decode(initializationVector),
      date: DateSerializer.serialize(this.encryptionKeyDate)
    };
    this.localStorageService.setItem('...', JSON.stringify(newState));
  });
}

İlk sorun, şifre çözme işleminden sonra DOMException almam. Bu neredeyse imkansız bir şekilde hata ayıklanıyor, çünkü gerçek hata güvenlik sorunları nedeniyle tarayıcı tarafından gizleniyor:

error: DOMException
code: 0
message: ""
name: "OperationError"

Diğer bir şey de yaklaşımımı sorguluyor olmam - şifreleme anahtarını böyle oluşturmak doğru mu? Sorunun kaynağı bunun olabileceğinden şüpheleniyorum, ama Web Crypto API kullanarak bir dizeden şifreleme anahtarı oluşturmanın hiçbir yolunu bulamadım.

Ayrıca, şifreleme anahtarı için kaynak olan dizi 128 karakter uzunluğunda, şu ana kadar sadece ilk 32 karakteri alarak 256 bit veri elde ediyorum. Bunun doğru olup olmadığından emin değilim, çünkü başlangıçtaki karakterler benzersiz olmayabilir. Burada hashleme iyi bir çözüm olabilir mi?

Her türlü yardım/rehberlik çok takdir edilecektir, özellikle de yaklaşımımı doğrulamak konusunda. Bu tür sorunlara dair herhangi bir örnek bulmakta zorlanıyorum. Teşekkür ederim!

Cevap #1

Uyarı Notu:

Uyarı: Bu API, bir dizi düşük seviyeli kriptografik ilkel sağlar. Onları yanlış kullanmak çok kolaydır ve ilgili tuzaklar oldukça ince olabilir.

Temel kriptografik işlevleri doğru bir şekilde kullandığınızı varsaysak bile, güvenli anahtar yönetimi ve genel güvenlik sistemi tasarımı doğru yapmak son derece zordur ve genellikle uzman güvenlik uzmanlarının alanına girer.

Güvenlik sistemi tasarımı ve uygulanmasındaki hatalar, sistemin güvenliğini tamamen etkisiz hale getirebilir.

Lütfen öğrenin ve denemeler yapın, ancak konu hakkında bilgili bir kişi bunu kapsamlı bir şekilde incelemeden önce çalışmanızın güvenliğini garanti etmeyin veya ima etmeyin. Crypto 101 Kursu, güvenli sistemlerin tasarımı ve uygulanması hakkında öğrenmeye başlamak için harika bir yer olabilir.

Ayrıca, ben bir uzman güvenlik uzmanı değilim. Tüm bunlar söylenmişken...


Bir yaklaşım, anahtarı arka uç sunucusundan benzersiz bir dize istemeden istemci tarafında oluşturmaktır. Bu anahtarla şifreleyin, anahtarı arka uç sunucunuza kaydedin ve ardından çözmek için anahtarı tekrar alın.

Bu JavaScript'te ve TypeScript'te de aynı şekilde çalışacaktır.

const runDemo = async () => {

  const messageOriginalDOMString = 'Do the messages match?';

  //
  // Encode the original data
  //

  const encoder = new TextEncoder();
  const messageUTF8 = encoder.encode(messageOriginalDOMString);

  //
  // Configure the encryption algorithm to use
  //

  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const algorithm = {
    iv,
    name: 'AES-GCM',
  };

  //
  // Generate/fetch the cryptographic key
  //

  const key = await window.crypto.subtle.generateKey({
      name: 'AES-GCM',
      length: 256
    },
    true, [
      'encrypt',
      'decrypt'
    ]
  );

  //
  // Run the encryption algorithm with the key and data.
  //

  const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
    algorithm,
    key,
    messageUTF8,
  );

  //
  // Export Key
  //
  const exportedKey = await window.crypto.subtle.exportKey(
    'raw',
    key,
  );
  
  // This is where to save the exported key to the back-end server,
  // and then to fetch the exported key from the back-end server.

  //
  // Import Key
  //
  const importedKey = await window.crypto.subtle.importKey(
    'raw',
    exportedKey,
    "AES-GCM",
    true, [
      "encrypt",
      "decrypt"
    ]
  );

  //
  // Run the decryption algorithm with the key and cyphertext.
  //

  const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
    algorithm,
    importedKey,
    messageEncryptedUTF8,
  );

  //
  // Decode the decryped data.
  //

  const decoder = new TextDecoder();
  const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);

  //
  // Assert
  //
  console.log(messageOriginalDOMString);
  console.log(messageDecryptedDOMString);

};

runDemo();

Öte yandan, eğer gereksinimler şifreleme anahtarının arka uçtan gelen benzersiz, düşük entropili bir dizeden türetilmesini gerektiriyorsa, o zaman deriveKey yöntemi PBKDF2 algoritması ile uygun olabilir.

const runDemo = async() => {

  const messageOriginalDOMString = 'Do the messages match?';

  //
  // Encode the original data
  //

  const encoder = new TextEncoder();
  const messageUTF8 = encoder.encode(messageOriginalDOMString);

  //
  // Configure the encryption algorithm to use
  //

  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const algorithm = {
    iv,
    name: 'AES-GCM',
  };

  //
  // Generate/fetch the cryptographic key
  //

  function getKeyMaterial() {
    let input = 'the-username' + new Date();
    let enc = new TextEncoder();
    return window.crypto.subtle.importKey(
      "raw",
      enc.encode(input), {
        name: "PBKDF2"
      },
      false, ["deriveBits", "deriveKey"]
    );
  }

  let keyMaterial = await getKeyMaterial();
  let salt = window.crypto.getRandomValues(new Uint8Array(16));

  let key = await window.crypto.subtle.deriveKey({
      "name": "PBKDF2",
      salt: salt,
      "iterations": 100000,
      "hash": "SHA-256"
    },
    keyMaterial, {
      "name": "AES-GCM",
      "length": 256
    },
    true, ["encrypt", "decrypt"]
  );

  //
  // Run the encryption algorithm with the key and data.
  //

  const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
    algorithm,
    key,
    messageUTF8,
  );

  //
  // Export Key
  //
  const exportedKey = await window.crypto.subtle.exportKey(
    'raw',
    key,
  );

  // This is where to save the exported key to the back-end server,
  // and then to fetch the exported key from the back-end server.

  //
  // Import Key
  //
  const importedKey = await window.crypto.subtle.importKey(
    'raw',
    exportedKey,
    "AES-GCM",
    true, [
      "encrypt",
      "decrypt"
    ]
  );

  //
  // Run the decryption algorithm with the key and cyphertext.
  //

  const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
    algorithm,
    importedKey,
    messageEncryptedUTF8,
  );

  //
  // Decode the decryped data.
  //

  const decoder = new TextDecoder();
  const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);

  //
  // Assert
  //
  console.log(messageOriginalDOMString);
  console.log(messageDecryptedDOMString);

};

runDemo();

İlgili makaleler