Web Crypto API で公開暗号鍵を用いてメッセージを送る


用途

  • 自分はサービス運営者で、ユーザー同士のメッセージ機能を作りたいが、その内容を知りたくない。

  • SlackやChatworkなどのメッセージングサービスを使ってパスワードなどを送りたいが、運営事業者様のサーバーにログが残るのが嫌だ。

要素技術

流れ

  1. 秘密を受け取る側:メッセージを受け取りたい側が鍵ペアを生成
  2. 秘密を受け取る側:既存のメッセージ機能を使って相手に公開鍵を送信
  3. 秘密を送る側:公開鍵を使ってメッセージを暗号化
  4. 秘密を送る側:既存のメッセージ機能を使って相手に暗号化したメッセージを送信
  5. 秘密を受け取る側:秘密鍵を使って複合する

コード

generate_key_pair.ts
let key: CryptoKeyPair;

// 鍵ペアを生成して保持する
// この関数の戻り値はJSONなので、Slackなど好きな手段で送る。
// https://github.com/diafygi/webcrypto-examples
async function createPublicKey(): Promise<JsonWebKey> {
   let key = await window.crypto.subtle.generateKey(
       {
           name: "RSA-OAEP",
           modulusLength: 2048, //can be 1024, 2048, or 4096
           publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
           hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
       },
       false, //whether the key is extractable (i.e. can be used in exportKey)
       ["encrypt", "decrypt"] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
   );
   // エクスポートとはJSオブジェクトである鍵をJSONに変換することのようだ
   return await window.crypto.subtle.exportKey(
       "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
       key.publicKey //can be a publicKey or privateKey, as long as extractable was true
   );
}
send_message.ts

// 受け取った公開鍵でメッセージを暗号化する
async function messageEncrypting(pubKeyJson: JsonWebKey): Promise<string> {
   // importとはJSON形式の公開鍵をJSオブジェクトに変換することのようだ
   let importKey = await window.crypto.subtle.importKey(
       "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
       pubKeyJson,
       {   //these are the algorithm options
           name: "RSA-OAEP",
           hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
       },
       false, //whether the key is extractable (i.e. can be used in exportKey)
       ["encrypt"] //"encrypt" or "wrapKey" for public key import or
       //"decrypt" or "unwrapKey" for private key imports
   );
   let message = window.prompt("メッセージを入力してください");

   let encMessage = await window.crypto.subtle.encrypt(
       {
           name: "RSA-OAEP",
           //label: Uint8Array([...]) //optional
       },
       importKey, //from generateKey or importKey above

       // stringをArrayBufferに変換しなければいけない
       string_to_buffer(message) //ArrayBuffer of data you want to encrypt
   );

   // encMessageはArrayBuffer形式なので、BASE64に変換しないとテキストチャット等で送ることができない
   // https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
   return btoa(String.fromCharCode.apply(null, new Uint8Array(encMessage)));
}


// https://gist.github.com/kawanet/352a2ed1d1656816b2bc
function string_to_buffer(src) {
   return (new Uint16Array([].map.call(src, function (c) {
       return c.charCodeAt(0)
   }))).buffer;
}
receive.ts
// 受信
async function receiveEncMessage(encMessage: string) {
   // stringをArrayBufferに変換しないといけない
   let msg = base64ToArrayBuffer(encMessage);
   let plain = await window.crypto.subtle.decrypt(
       {
           name: "RSA-OAEP",
       },
       key.privateKey, //from generateKey or importKey above
       msg //ArrayBuffer of the data
   );
   window.alert(buffer_to_string(plain));
}

function buffer_to_string(buf) {
   return String.fromCharCode.apply("", new Uint16Array(buf))
}

function base64ToArrayBuffer(base64) {
    var binary_string =  window.atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array( len );
    for (var i = 0; i < len; i++)        {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}