API Kriptografi Web: Studi Kasus

Selamat siang teman!



Dalam tutorial ini, kita akan melihat API Kriptografi Web : antarmuka enkripsi data sisi klien. Tutorial ini didasarkan pada artikel ini . Diasumsikan bahwa Anda cukup familiar dengan enkripsi.



Apa sebenarnya yang akan kita lakukan? Kami akan menulis server sederhana yang akan menerima data terenkripsi dari klien dan mengembalikannya berdasarkan permintaan. Datanya sendiri akan diproses di sisi klien.



Server akan diimplementasikan di Node.js menggunakan Express, klien di JavaScript. Bootstrap akan digunakan untuk penataan.



Kode proyek ada di sini .



Jika Anda tertarik, silakan ikuti saya.



Latihan



Buat direktori crypto-tut:



mkdir crypto-tut


Kami masuk ke dalamnya dan menginisialisasi proyek:



cd crypto-tut

npm init -y


Pasang express:



npm i express


Pasang nodemon:



npm i -D nodemon


Mengedit package.json:



"main": "server.js",
"scripts": {
    "start": "nodemon"
},


Struktur proyek:



crypto-tut
    --node_modules
    --src
        --client.js
        --index.html
        --style.css
    --package-lock.json
    --package.json
    --server.js


Isi index.html:



<head>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <script src="client.js" defer></source>
</head>

<body>
    <div class="container">
        <h3>Web Cryptography API Tutorial</h3>
        <input type="text" value="Hello, World!" class="form-control">
        <div class="btn-box">
            <button class="btn btn-primary btn-send">Send message</button>
            <button class="btn btn-success btn-get" disabled>Get message</button>
        </div>
        <output></output>
    </div>
</body>


Isi style.css:



h3,
.btn-box {
    margin: .5em;
    text-align: center;
}

input,
output {
    display: block;
    margin: 1em auto;
    text-align: center;
}

output span {
    color: green;
}


Server



Mari mulai membuat server.



Kami buka server.js.



Kami menghubungkan ekspres dan membuat contoh aplikasi dan router:



const express = require('express')
const app = express()
const router = express.Router()


Kami menghubungkan middleware (lapisan perantara antara permintaan dan respons):



//  
app.use(express.json({
    type: ['application/json', 'text/plain']
}))
//  
app.use(router)
//    
app.use(express.static('src'))


Kami membuat variabel untuk menyimpan data:



let data


Kami memproses penerimaan data dari klien:



router.post('/secure-api', (req, res) => {
    //     
    data = req.body
    //    
    console.log(data)
    //  
    res.end()
})


Kami memproses pengiriman data ke klien:



router.get('/secure-api', (req, res) => {
    //     JSON,
    //     
    res.json(data)
})


Kami memulai server:



app.listen(3000, () => console.log('Server ready'))


Kami menjalankan perintah npm start. Terminal menampilkan pesan "Server siap". Pembukaan http://localhost:3000:







Di sinilah kita selesai dengan server, pergi ke sisi klien aplikasi.



Klien



Di sinilah kesenangan dimulai.



Buka file client.js.



Algoritma simetris AES-GCM akan digunakan untuk enkripsi data. Algoritme semacam itu memungkinkan penggunaan kunci yang sama untuk enkripsi dan dekripsi.



Buat fungsi pembangkitan kunci simetris:



// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey
const generateKey = async () =>
    window.crypto.subtle.generateKey({
        name: 'AES-GCM',
        length: 256,
    }, true, ['encrypt', 'decrypt'])


Data harus dikodekan menjadi aliran byte sebelum dienkripsi. Ini mudah dilakukan dengan kelas TextEncoder:



// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
const encode = data => {
    const encoder = new TextEncoder()

    return encoder.encode(data)
}


Selanjutnya, kita membutuhkan vektor eksekusi (vektor inisialisasi, IV), yang merupakan urutan karakter acak atau pseudo-random yang ditambahkan ke kunci enkripsi untuk meningkatkan keamanannya:



// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const generateIv = () =>
    // https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
    window.crypto.getRandomValues(new Uint8Array(12))


Setelah membuat fungsi helper, kita dapat mengimplementasikan fungsi enkripsi. Fungsi ini harus mengembalikan sandi dan IV sehingga sandi dapat diuraikan selanjutnya:



const encrypt = async (data, key) => {
    const encoded = encode(data)
    const iv = generateIv()
    const cipher = await window.crypto.subtle.encrypt({
        name: 'AES-GCM',
        iv
    }, key, encoded)

    return {
            cipher,
            iv
        }
}


Setelah mengenkripsi data dengan SubtleCrypto , mereka adalah penyangga data biner mentah. Ini bukan format terbaik untuk transmisi dan penyimpanan. Mari perbaiki ini.



Data tersebut biasanya dikirim dalam format JSON dan disimpan dalam database. Oleh karena itu, masuk akal untuk mengemas data ke dalam format portabel. Salah satu cara untuk melakukannya adalah dengan mengonversi data menjadi string base64:



// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
const pack = buffer => window.btoa(
    String.fromCharCode.apply(null, new Uint8Array(buffer))
)


Setelah menerima data, perlu dilakukan proses sebaliknya, yaitu. mengonversi string yang dikodekan base64 menjadi buffer biner mentah:



// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
const unpack = packed => {
    const string = window.atob(packed)
    const buffer = new ArrayBuffer(string.length)
    const bufferView = new Uint8Array(buffer)

    for (let i = 0; i < string.length; i++) {
        bufferView[i] = string.charCodeAt(i)
    }

    return buffer
}


Tetap menguraikan data yang diperoleh. Namun, setelah dekripsi, kita perlu mendekode aliran byte ke dalam format aslinya. Ini dapat dilakukan menggunakan kelas TextDecoder:



// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
const decode = byteStream => {
    const decoder = new TextDecoder()

    return decoder.decode(byteStream)
}


Fungsi dekripsi adalah kebalikan dari fungsi enkripsi:



// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt
const decrypt = async (cipher, key, iv) => {
    const encoded = await window.crypto.subtle.decrypt({
        name: 'AES-GCM',
        iv
    }, key, cipher)

    return decode(encoded)
}


Pada tahap ini, kontennya client.jsterlihat seperti ini:



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

const encode = data => {
    const encoder = new TextEncoder()

    return encoder.encode(data)
}

const generateIv = () =>
    window.crypto.getRandomValues(new Uint8Array(12))

const encrypt = async (data, key) => {
    const encoded = encode(data)
    const iv = generateIv()
    const cipher = await window.crypto.subtle.encrypt({
        name: 'AES-GCM',
        iv
    }, key, encoded)

    return {
        cipher,
        iv
    }
}

const pack = buffer => window.btoa(
    String.fromCharCode.apply(null, new Uint8Array(buffer))
)

const unpack = packed => {
    const string = window.atob(packed)
    const buffer = new ArrayBuffer(string.length)
    const bufferView = new Uint8Array(buffer)

    for (let i = 0; i < string.length; i++) {
        bufferView[i] = string.charCodeAt(i)
    }

    return buffer
}

const decode = byteStream => {
    const decoder = new TextDecoder()

    return decoder.decode(byteStream)
}

const decrypt = async (cipher, key, iv) => {
    const encoded = await window.crypto.subtle.decrypt({
        name: 'AES-GCM',
        iv
    }, key, cipher)

    return decode(encoded)
}


Sekarang mari kita implementasikan pengiriman dan penerimaan data.



Kami membuat variabel:



//    ,   
const input = document.querySelector('input')
//    
const output = document.querySelector('output')

// 
let key


Enkripsi dan pengiriman data:



const encryptAndSendMsg = async () => {
    const msg = input.value

     // 
    key = await generateKey()

    const {
        cipher,
        iv
    } = await encrypt(msg, key)

    //   
    await fetch('http://localhost:3000/secure-api', {
        method: 'POST',
        body: JSON.stringify({
            cipher: pack(cipher),
            iv: pack(iv)
        })
    })

    output.innerHTML = ` <span>"${msg}"</span> .<br>   .`
}


Menerima dan mendekripsi data:



const getAndDecryptMsg = async () => {
    const res = await fetch('http://localhost:3000/secure-api')

    const data = await res.json()

    //    
    console.log(data)

    //   
    const msg = await decrypt(unpack(data.cipher), key, unpack(data.iv))

    output.innerHTML = `   .<br> <span>"${msg}"</span> .`
}


Menangani klik tombol:



document.querySelector('.btn-box').addEventListener('click', e => {
    if (e.target.classList.contains('btn-send')) {
        encryptAndSendMsg()

        e.target.nextElementSibling.removeAttribute('disabled')
    } else if (e.target.classList.contains('btn-get')) {
        getAndDecryptMsg()
    }
})


Mulai ulang server untuk berjaga-jaga. Kami buka http://localhost:3000. Klik pada tombol "Kirim pesan":







Kami melihat data yang diterima oleh server di terminal:



{
  cipher: 'j8XqWlLIrFxyfA2easXkJTLLIt9x8zLHei/tTKI=',
  iv: 'F8doVULJzbEQs3M1'
}


Klik pada tombol "Dapatkan pesan":







Kami melihat data yang sama yang diterima oleh klien di konsol:



{
  cipher: 'j8XqWlLIrFxyfA2easXkJTLLIt9x8zLHei/tTKI=',
  iv: 'F8doVULJzbEQs3M1'
}


API Kriptografi Web membuka peluang menarik bagi kami untuk melindungi informasi rahasia di sisi klien. Langkah lain menuju pengembangan web tanpa server.



Dukungan untuk teknologi ini saat ini 96%:







Saya harap Anda menikmati artikelnya. Terima kasih atas perhatian Anda.



All Articles