26 Mei 2019
Implementasi Kriptografi Asimetris dengan JavaScript dan PHP

Tulisan kali ini akan membahas teknik implementasi kriptografi asimetris dengan JavaScript dan PHP. Algoritma kriptografi yang akan digunakan disini adalah RSA dengan memanfaatkan library CryptoJS, JSEncrypt dan OpenSSL. Studi kasus yang akan kita gunakan disini adalah proses pengiriman data menggunakan HTTP Post dari halaman web di sisi client ke kode PHP di sisi server.

Pengantar

Kriptografi Asimetris

Kriptografi asimetris atau kriptografi kunci public merupakan sistem kriptografi yang menggunakan dua kunci yang berbeda, kunci publik dan kunci pribadi, untuk proses enkripsi dan dekripsi. Kunci publik adalah kunci yang dapat dimiliki oleh umum selaku pihak pengirim (sender), digunakan hanya untuk mengenkripsi pesan sebelum pesan tersebut dikirim. Kunci pribadi (private key) adalah kunci yang hanya dimiliki satu pihak yaitu penerima (receiver) dan digunakan hanya untuk mendekripsi ciphertext yang diterima.

RSA

RSA (Rivest-Shamir-Adleman) merupakan salah satu algoritma kriptografi asimetris yang banyak digunakan dalam perangkat lunak untuk proses transmisi data. Algoritma RSA berbasis pada tingkat kesulitan dalam faktorisasi hasil dari dua bilangan prima berukuran besar. Dalam implementasinya, algoritma ini jarang digunakan untuk mengenkripsi secara langsung, disebabkan karena RSA hanya bisa mengenkripsi data dengan ukuran tidak lebih besar dari ukuran kunci publik.

Metode yang umum digunakan adalah mengenkripsi data dengan suatu algoritma kriptografi simetris (sebagai contoh AES) menggunakan kunci yang di-generate secara acak. Kunci tersebut yang kemudian dienkripsi dengan RSA dengan kunci publik.

Pembangkitan Kunci

Sebelum memulai proses pengkodean, kita perlu membangkitkan kunci RSA untuk proses enkripsi dan dekripsi nantinya. Tool yang digunakan disini adalah openssl yang tersedia dalam OS varian linux atau bisa diinstall melalui Cygwin untuk OS Windows.

Perintah yang digunakan adalah sebagai berikut :

openssl genrsa -out privatekey.txt 2048

Perintah diatas akan menghasilkan kunci pribadi yang disimpan dalam file privatekey.txt. Dari kunci pribadi tersebut, kita bangkitkan kunci publik dengan menggunakan perintah dibawah :

openssl rsa -pubout -in privatekey.txt -out publickey.txt

Selain dengan cara diatas, kedua kunci RSA bisa didapatkan secara online melalui website-website yang menyediakan layanan pembangkitan kunci RSA, contoh website tersebut antara lain :

  1. http://csfieldguide.org.nz/en/interactives/rsa-key-generator/index.html
  2. https://8gwifi.org/RSAFunctionality?keysize=2048
  3. http://travistidwell.com/jsencrypt/demo/index.html

Kunci publik disimpan pada direktori web agar bisa diakses oleh client, sedangkan kunci pribadi disimpan pada direktori lain, sebagai contoh di C:\key.

Pengkodean

Sebagaimana disebutkan sebelumnya, disini kita akan menggunakan beberapa library. Di sisi client, kita akan gunakan library javascript yaitu CryptoJS dan JSEncrypt, sedangkan di sisi server, kita akan gunakan library PHP OpenSSL.

Sisi Client

Kode HTML untuk halaman web (nama file index.html) yang akan digunakan adalah sebagai berikut :

<!DOCTYPE html>
<html lang="id">
  <head>
    <title>RSA Example</title>
    <style type="text/css">
      body { font-family: arial, sans, helvetica; font-size: 80%; }
      fieldset { border: 1px solid; background-color: #f0f0ff; padding: 15px; }
      fieldset > legend { font-weight: bold; }
      .container { padding: 5em; }
      .field { margin-bottom: 5px; }
      .field > label { display: inline-block; width: 75px; text-align: right; margin-right: 5px; }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/2.3.1/jsencrypt.js"></script>
  </head>
  <body>
    <div class="container">
      <h1>RSA Encryption Example</h1>
      <fieldset>
        <legend>Masukkan Informasi Kontak</legend>
        <form class="form">
          <div class="field">
            <label>Nama :</label>
            <input type="text" id="nama"/>
          </div>
          <div class="field">
            <label>Alamat :</label>
            <input type="text" id="alamat"/>
          </div>
          <div class="field">
            <label>No Telepon :</label>
            <input type="text" id="notelp"/>
          </div>
          <div class="field">
            <label>E-Mail :</label>
            <input type="text" id="email"/>
          </div>
          <div class="field">
            <button id="btnSave">Save</button>
          </div>
        </form>
      </fieldset>
    </div>
    <script src="script.js"></script>
  </body>
</html>

Setelah halaman web selesai di-load, kode javascript akan memeriksa ketersediaan kunci publik didalam browser. Kode javascript akan mengirimkan request untuk mendapatkan kunci publik jika tidak tersedia, kemudian menyimpan kunci tersebut dalam session storage dengan id pubkey.

$(document).ready(() => {
  if(typeof(Storage) !== 'undefined') {
    if(sessionStorage.getItem('pubkey') === null) {
      jQuery.get('/publickey.txt', (data) => {
        sessionStorage.setItem('pubkey', data);
      });
    }
  }
  else {
    console.log('Penyimpanan lokal tidak tersedia dalam browser ini');
  }
});

Proses enkripsi dan pengiriman data dipicu pada saat user mengklik tombol Save.

$('#btnSave').on('click', (e) => {
  // kode berikut ditulis disini
});

Di tahap awal, kita susun data kontak dalam bentuk objek javascript kemudian memeriksa kelengkapan data tersebut. Jika terdapat field yang kosong maka proses akan dibatalkan

let kontak = {
  nama: $('#nama').val(),
  alamat: $('#alamat').val(),
  noTelepon: $('#notelp').val(),
  email: $('#email').val()
};
if((kontak.nama==='')||(kontak.alamat==='')||(kontak.noTelepon==='')||(kontak.email==='')) {
  return false;
}

Berikutnya, kita ambil kunci publik yang disimpan sebelumnya di session storage dengan menyebutkan id pubkey yang sudah ditentukan. Jika kunci tersebut tidak ditemukan (diindikasikan dengan nilai null) maka proses akan dibatalkan

let pubkey = sessionStorage.getItem('pubkey');
if(pubkey === null) {
  return false;
}

Dari sini kita bisa lanjut ke proses enkripsi data menggunakan library CryptoJS. Algoritma yang digunakan adalah AES dengan kunci dan nilai initialization vector (IV) dibangkitkan secara acak.

let key = CryptoJS.lib.WordArray.random(16);
let iv = CryptoJS.lib.WordArray.random(16);
let enc = CryptoJS.AES.encrypt(JSON.stringify(kontak), key, { iv: iv });

Kedua nilai kunci dan iv kemudian dienkripsi dengan library RSA JSEncrypt menggunakan kunci publik pubkey. Disini juga kita akan menyusun data yang akan dikirim ke sisi server

let jse = new JSEncrypt();
jse.setPublicKey(pubkey);
let payload = {
  cipher: enc.toString(),
  iv: jse.encrypt(iv.toString(CryptoJS.enc.Base64)),
  key: jse.encrypt(key.toString(CryptoJS.enc.Base64))
};

Terakhir kita tinggal kirimkan data ke sisi server menggunakan HTTP POST dengan fungsi jQuery.ajax()

jQuery.ajax({
  url: '/handler.php',
  method: 'POST',
  data: JSON.stringify(payload),
  dataType: 'json',
  contentType: 'application/json',
  success: (data, status, xhr) => {
    console.log(data.data);
    alert(data.message);
  },
  error: (xhr, status, err) => {
    let t = JSON.parse(xhr.responseText);
    alert(t.message);
  }
});

Kode javascript selengkapnya adalah sebagai berikut :

$(document).ready(() => {
  if(typeof(Storage) !== 'undefined') {
    if(sessionStorage.getItem('pubkey') === null) {
      jQuery.get('/publickey.txt', (data) => {
        sessionStorage.setItem('pubkey', data);
      });
    }
  }
  else {
    console.log('Penyimpanan lokal tidak tersedia dalam browser ini');
  }
});

$('#btnSave').on('click', (e) => {
  let kontak = {
    nama: $('#nama').val(),
    alamat: $('#alamat').val(),
    noTelepon: $('#notelp').val(),
    email: $('#email').val()
  };
  if((kontak.nama==='')||(kontak.alamat==='')||(kontak.noTelepon==='')||(kontak.email==='')) {
    return false;
  }
  let pubkey = sessionStorage.getItem('pubkey');
  if(pubkey === null) {
    return false;
  }
  let key = CryptoJS.lib.WordArray.random(16);
  let iv = CryptoJS.lib.WordArray.random(16);
  let enc = CryptoJS.AES.encrypt(JSON.stringify(kontak), key, { iv: iv });
  let jse = new JSEncrypt();
  jse.setPublicKey(pubkey);
  let payload = {
    cipher: enc.toString(),
    iv: jse.encrypt(iv.toString(CryptoJS.enc.Base64)),
    key: jse.encrypt(key.toString(CryptoJS.enc.Base64))
  };
  jQuery.ajax({
    url: '/handler.php',
    method: 'POST',
    data: JSON.stringify(payload),
    dataType: 'json',
    contentType: 'application/json',
    success: (data, status, xhr) => {
      console.log(data.data);
      alert(data.message);
    },
    error: (xhr, status, err) => {
      let t = JSON.parse(xhr.responseText);
      alert(t.message);
    }
  });
});

Sisi Server

Tipe dari konten yang akan dikembalikan dari server adalah JSON

header("Content-Type: application/json");

URL ini hanya akan melayani metode HTTP POST. Selain dari metode tersebut, server akan mengembalikan error 405 dengan pesan "Method not allowed".

if($_SERVER["REQUEST_METHOD"] != "POST") {
  http_response_code(405);
  echo json_encode(["message" => "method not allowed"]);
  return false;
}

Selanjutnya adalah tahap pembacaan dan konversi data yang dikirim dari client dari JSON menjadi associative array.

$body = json_decode(file_get_contents("php://input"), $assoc=true);

Berikutnya kita lanjutkan ke proses pembacaan kunci pribadi yang tersimpan dalam file c:\key\privatekey.txt. Jika file ini tidak ditemukan, server akan mengembalikan error 500 dengan pesan "Private key doesn't exists".

$privkey_file = "c:\\key\\privatekey.txt";
$f = @fopen($privkey_file, "r");
if(!$f) {
  http_response_code(500);
  echo json_encode(["message" => "Private key doesn't exists"]);
  return false;
}
$privkey = fread($f, filesize($privkey_file));
fclose($f);

Dari sini kita bisa mendekripsi data yang dikirim dari client. Urutan proses dekripsi dimulai dari nilai kunci dan initialization vector, yang mana keduanya tersimpan pada $body["key"] dan $body["iv"] dengan format encoding Base64 (perhatikan variabel payload di kode javascript sebelumnya). Kedua nilai tersebut didekripsi menggunakan kunci pribadi $privkey.

Fungsi yang digunakan disini adalah openssl_private_decrypt() yang mengembalikan nilai boolean yang mengindikasikan berhasil atau tidaknya proses dekripsi. Jika proses gagal, server akan mengembalikan error 500 dengan pesan kegagalan didapatkan dari fungsi openssl_error_string(). Hasil dekripsi dari kedua nilai tersebut tersimpan pada variabel $_key dan $_iv.

if(!openssl_private_decrypt(base64_decode($body["key"]), $_key, $privkey)) {
  http_response_code(500);
  echo json_encode(["message" => openssl_error_string()]);
  return false;
}

if(!openssl_private_decrypt(base64_decode($body["iv"]), $_iv, $privkey)) {
  http_response_code(500);
  echo json_encode(["message" => openssl_error_string()]);
  return false;
}

Proses dekripsi berikutnya adalah terhadap nilai variabel $body["cipher"] untuk mendapatkan data kontak yang diinput oleh user. Fungsi yang akan digunakan disini adalah openssl_decrypt() dengan menggunakan algoritma AES serta nilai $_key dan $_iv hasil proses dekripsi awal sebelumnya. Fungsi openssl_decrypt akan mengembalikan plain text dari data yang dikirim atau nilai boolean false jika proses dekripsi gagal.

Sama seperti sebelumnya, jika proses ini gagal, server akan mengembalikan error 500 dengan pesan didapatkan dari fungsi openssl_error_string().

if(!($plain = @openssl_decrypt(base64_decode($body["cipher"]), "AES-128-CBC", base64_decode($_key), OPENSSL_RAW_DATA, base64_decode($_iv)))) {
  http_response_code(500);
  echo json_encode(["message" => openssl_error_string(), "line" => 36]);
  return false;
}

Sampai tahap ini kita sudah mendapatkan data kontak yang dikirimkan client dalam format JSON. Selebihnya kita tinggal mengkonversi data tersebut menjadi associative array serta menotifikasi user bahwa data berhasil diterima

$data = json_decode($plain, $assoc=true);
echo json_encode([
  "message" => "Server berhasil menerima data anda. Cek data yang anda kirim di konsol browser",
  "data" => [
    "nama" => $data["nama"],
    "alamat" => $data["alamat"],
    "no_telepon" => $data["noTelepon"],
    "email" => $data["email"]
  ]
]);

Kode PHP selengkapnya adalah sebagai berikut :

<?php

  header("Content-Type: application/json");

  if($_SERVER["REQUEST_METHOD"] != "POST") {
    http_response_code(405);
    echo json_encode(["message" => "method not allowed"]);
    return false;
  }

  $body = json_decode(file_get_contents("php://input"), $assoc=true);

  $privkey_file = "c:\\key\\privatekey.txt";
  $f = @fopen($privkey_file, "r");
  if(!$f) {
    http_response_code(500);
    echo json_encode(["message" => "Private key doesn't exists"]);
    return false;
  }
  $privkey = fread($f, filesize($privkey_file));
  fclose($f);

  if(!openssl_private_decrypt(base64_decode($body["key"]), $_key, $privkey)) {
    http_response_code(500);
    echo json_encode(["message" => openssl_error_string(), "line" => 24]);
    return false;
  }

  if(!openssl_private_decrypt(base64_decode($body["iv"]), $_iv, $privkey)) {
    http_response_code(500);
    echo json_encode(["message" => openssl_error_string(), "line" => 30]);
    return false;
  }

  if(!($plain = @openssl_decrypt(base64_decode($body["cipher"]), "AES-128-CBC", base64_decode($_key), OPENSSL_RAW_DATA, base64_decode($_iv)))) {
    http_response_code(500);
    echo json_encode(["message" => openssl_error_string(), "line" => 36]);
    return false;
  }

  $data = json_decode($plain, $assoc=true);

  echo json_encode([
    "message" => "Server berhasil menerima data anda. Cek data yang anda kirim di konsol browser",
    "data" => [
      "nama" => $data["nama"],
      "alamat" => $data["alamat"],
      "no_telepon" => $data["noTelepon"],
      "email" => $data["email"]
    ]
  ]);

?>