Jekyll2023-10-08T15:53:59+00:00https://blog.jocki.me/feed.xmlCatatan JockiA Programmer's JourneyJocki HendryUSB Ethernet Adapter Tidak Terdeteksi Di Raspberry Pi OS2023-10-08T00:00:00+00:002023-10-08T00:00:00+00:00https://blog.jocki.me/os/2023/10/08/usb-ethernet-tidak-terdeteksi<p>Pada suatu hari, saya menggunakan sebuah papan Raspberry Pi untuk dijadikan sebagai perangkat jaringan seperti <em>router</em>,
IDS, <em>firewall</em>, <em>VPN gateway</em> dan sebagainya. Seperti perangkat jaringan lain pada umumnya, saya membutuhkan dua
<em>port</em> RJ45 yang berbeda: satu untuk masukan yang dihubungkan ke <em>switch</em> dan satu lagi untuk keluaran yang dihubungkan
ke Internet. Karena Raspberry Pi hanya memiliki sebuah <em>port</em> RJ45, saya terpaksa menggunakan USB Ethernet Adapter untuk
menambahkan sebuah <em>port</em> RJ45 baru lewat USB 3. Semua berjalan sesuai dengan harapan dan lancar hingga suatu hari
perangkat Raspberry Pi tersebut <em>restart</em> akibat pemadaman listrik. Sejak itu, perangkat USB Ethernet Adapter tersebut
tiba-tiba tidak terdeteksi lagi. Bila saya memasang ulang perangkat ke port USB yang berbeda, perangkat akan kembali
terdeteksi. Namun saya tidak bisa selalu melakukan ini setiap kali <em>restart</em> karena saya tidak selalu berada di lokasi
fisik yang sama.</p>
<p><img src="/assets/images/gambar_00111.png" alt="Raspberry Pi Dengan USB Ethernet Adapter" class="img-fluid rounded" /></p>
<p>Langkah pertama yang saya melakukan untuk melakukan <em>troubleshooting</em> perangkat USB adalah dengan menjalankan perintah <code class="language-plaintext highlighter-rouge">lsusb</code>.
Sebagai contoh, saya mengetikkan perintah seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>lsusb -t</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
|__ Port 1: Dev 2, If 0, Class=Mass Storage, Driver=usb-storage, 5000M
/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
</code></pre></div></div>
<p>Satu hal yang menarik perhatian saya pada hasil di atas adalah perangkat di <code class="language-plaintext highlighter-rouge">Bus 02.Port 1</code> dengan nilai <code class="language-plaintext highlighter-rouge">Class</code> berupa
<code class="language-plaintext highlighter-rouge">Mass Storage</code> dan <code class="language-plaintext highlighter-rouge">Driver</code> berupa <code class="language-plaintext highlighter-rouge">usb-storage</code>. Bila saya menampilkan informasi khusus untuk perangkat tersebut (berdasarkan
nomor bus dan nomor perangkat), saya menemukan hasil seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>lsusb -s 2:2</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bus 002 Device 002: ID 2357:8151 TP-Link USB 10/100/1000 LAN
</code></pre></div></div>
<p>Bila dilihat dari nilai <em>vendor id</em> <code class="language-plaintext highlighter-rouge">2357</code>, ini adalah benar produsen USB Ethernet <em>adapter</em> yang saya pakai. Namun kenapa terdeteksi
sebagai <em>mass storage</em>? Untuk mendapatkan informasi yang lebih detail, saya dapat menambahkan <code class="language-plaintext highlighter-rouge">-v</code> pada perintah <code class="language-plaintext highlighter-rouge">lsusb</code> seperti
berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>lsusb -vs 2:2</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
bInterfaceClass 8 Mass Storage
bInterfaceSubClass 6 SCSI
bInterfaceProtocol 80 Bulk-Only
...
</code></pre></div></div>
<p>SCSI merupakan <em>standard</em> yang sudah tidak populer lagi di dunia PC (<em>Personal Computer</em> atau komputer pribadi) dimana SCSI
digantikan oleh IDE yang selanjutnya diteruskan oleh SATA sebagai <em>interface</em> paling populer untuk media penyimpanan saat ini. Untuk
melihat nama perangkat penyimpan SCSI, saya bisa menggunakan perintah <code class="language-plaintext highlighter-rouge">lsblk</code> seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>lsblk --scsi</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME HCTL TYPE VENDOR MODEL REV SERIAL TRAN
sr0 0:0:0:0 rom Realtek USB_CD-ROM 2.00 000001 usb
</code></pre></div></div>
<p>Hasil perintah di atas menunjukkan bahwa perangkat penyimpanan ini ada di <code class="language-plaintext highlighter-rouge">/dev/sr0</code>. Untuk melihat isinya, saya bisa menggunakan
perintah seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>mkdir /mnt/cdrom</code></p>
</blockquote>
<blockquote>
<p><strong>$</strong> <code>mount /dev/sr0 /mnt/cdrom</code></p>
</blockquote>
<p>Setelah ini, untuk mendapatkan daftar file, saya akan memberikan perintah seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>ls /mnt/cdrom</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TP-LINK.ico TP-LINK_Gigabit_Ethernet_USB_Adapter.exe autorun.inf
</code></pre></div></div>
<p>Sepertinya media penyimpanan ini berisi driver untuk sistem operasi Windows yang perlu di-<em>install</em> sebelum perangkat bisa dipakai. Tetapi
saya tidak perlu melakukan ini di Linux. Untuk membuat perangkat ini kembali terdeteksi sebagai perangkat jaringan,
saya dapat menggunakan perintah <code class="language-plaintext highlighter-rouge">usbreset</code> seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>usbreset 2357:8151</code></p>
</blockquote>
<p>Sekarang bila saya memberikan perintah <code class="language-plaintext highlighter-rouge">lsusb</code>, saya akan menemukan perangkat tersebut memiliki nomor produk yang berbeda dari
sebelumnya, seperti yang terlihat pada hasil eksekusi perintah berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>lsusb -t</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
|__ Port 1: Dev 3, If 0, Class=Vendor Specific Class, Driver=r8152, 5000M
/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
</code></pre></div></div>
<blockquote>
<p><strong>$</strong> <code>lsusb -s 2:3</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bus 002 Device 003: ID 2357:0601 TP-Link UE300 10/100/1000 LAN (ethernet mode) [Realtek RTL8153]
</code></pre></div></div>
<p>Sekarang, perangkat sudah terdeteksi sebagai perangkat <em>ethernet</em> dimana saya bisa memakainya sebagai <code class="language-plaintext highlighter-rouge">eth1</code>. Ini adalah apa
yang saya harapkan. Walaupun demikian, bila saya men-<em>restart</em> Raspberry Pi yang saya pakai, perangkat <em>ethernet</em> ini akan
kembali hilang dan berubah menjadi <em>mass storage</em>. Bagaimana caranya supaya perubahan ini permanen?</p>
<p>Solusi yang paling cepat terpikirkan adalah dengan menjalankan <code class="language-plaintext highlighter-rouge">usbreset</code> secara otomatis melalui <em>script</em> saat Raspberry Pi
dinyalakan. Namun, setelah melakukan penelusuran lebih lanjut, saya menemukan solusi yang lebih sederhana. Berdasarkan
informasi yang ada di <a href="https://github.com/raspberrypi/rpi-eeprom/issues/472">https://github.com/raspberrypi/rpi-eeprom/issues/472</a>, beberapa pengguna yang mengalami hal ini melaporkan
hasil yang baik bila <code class="language-plaintext highlighter-rouge">NET_INSTALL_ENABLED=0</code> ditambahkan pada file konfigurasi yang dipakai oleh <em>bootloader</em> di EEPROM.</p>
<p>Untuk melakukan perubahan pada file konfigurasi EEPROM, saya dapat menggunakan perintah <code class="language-plaintext highlighter-rouge">rpi-eeprom-config</code> seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>sudo EDITOR=vim rpi-eeprom-config --edit</code></p>
</blockquote>
<p>Setelah itu akan muncul <em>text editor</em> dimana saya akan menambahkan sebuah baris baru dengan nilai <code class="language-plaintext highlighter-rouge">NET_INSTALL_ENABLED=0</code> sehingga
isinya menjadi seperti:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[all]
BOOT_UART=0
WAKE_ON_GPIO=1
POWER_OFF_ON_HALT=0
NET_INSTALL_ENABLED=0
</code></pre></div></div>
<p>Setelah menyimpan perubahan file tersebut dan memberikan perintah <code class="language-plaintext highlighter-rouge">reboot</code> untuk menjalankan ulang Raspberry Pi OS, saya
menemukan bahwa perangkat USB Ethernet Adapter yang saya pakai kini dapat terdeteksi secara otomatis sebagai perangkat jaringan, bukan
lagi sebagai <em>mass storage</em>.</p>Jocki HendryPada suatu hari, saya menggunakan sebuah papan Raspberry Pi untuk dijadikan sebagai perangkat jaringan seperti router, IDS, firewall, VPN gateway dan sebagainya. Seperti perangkat jaringan lain pada umumnya, saya membutuhkan dua port RJ45 yang berbeda: satu untuk masukan yang dihubungkan ke switch dan satu lagi untuk keluaran yang dihubungkan ke Internet. Karena Raspberry Pi hanya memiliki sebuah port RJ45, saya terpaksa menggunakan USB Ethernet Adapter untuk menambahkan sebuah port RJ45 baru lewat USB 3. Semua berjalan sesuai dengan harapan dan lancar hingga suatu hari perangkat Raspberry Pi tersebut restart akibat pemadaman listrik. Sejak itu, perangkat USB Ethernet Adapter tersebut tiba-tiba tidak terdeteksi lagi. Bila saya memasang ulang perangkat ke port USB yang berbeda, perangkat akan kembali terdeteksi. Namun saya tidak bisa selalu melakukan ini setiap kali restart karena saya tidak selalu berada di lokasi fisik yang sama.Apa Itu Protokol Traversal Using Relays around NAT (TURN)?2023-09-30T00:00:00+00:002023-09-30T00:00:00+00:00https://blog.jocki.me/network/2023/09/30/apa-itu-protokol-turn<p>Traversal Using Relays around NAT (TURN) adalah sebuah protokol <em>relay</em> yang memungkinkan <em>client</em> berkomunikasi dengan <em>peer</em>
yang tidak memiliki IP publik secara langsung (misalnya berada dibalik NAT). Protokol ini didefinisikan di <a href="https://www.ietf.org/rfc/rfc8656.txt">RFC 8656</a>.
Komponen TURN terdiri atas TURN client dan TURN server. Komunikasi antara TURN client dengan <em>peer</em> selalu melalui TURN server yang berperan
sebagai perantara. Oleh sebab itu, TURN server dan <em>peer</em> harus bisa saling berkomunikasi yang biasanya dilakukan
dengan meletakkan TURN server pada jaringan publik.</p>
<p>Untuk struktur <em>packet</em>, TURN sendiri merupakan ekstensi dari STUN. Struktur <em>packet</em> TURN tetap mengikuti struktur
<em>packet</em> STUN seperti yang saya tulis pada <a href="https://blog.jocki.me/network/2023/09/06/apa-itu-protokol-stun">artikel sebelumnya</a>.
Yang berbeda adalah TURN menambahkan beberapa operasi baru pada STUN seperti <code class="language-plaintext highlighter-rouge">Allocate</code>, <code class="language-plaintext highlighter-rouge">Refresh</code>, <code class="language-plaintext highlighter-rouge">Send</code>, <code class="language-plaintext highlighter-rouge">Data</code>,
<code class="language-plaintext highlighter-rouge">CreatePermission</code> dan <code class="language-plaintext highlighter-rouge">ChannelBind</code>. Selain itu, TURN juga menambahkan atribut baru di STUN seperti <code class="language-plaintext highlighter-rouge">XOR-PEER-ADDRESS</code>,
<code class="language-plaintext highlighter-rouge">REQUESTED-TRANSPORT</code>, <code class="language-plaintext highlighter-rouge">DATA</code>, dan sebagainya.</p>
<p>Sebagai latihan, saya akan membuat aplikasi yang mengerjakan perintah terminal Linux secara jarak jauh dimana terdapat
dimana perintah yang sama yang diberikan pengguna akan dikerjakan oleh dua server Linux secara bersamaan. Komponen aplikasi
latihan ini terdiri atas:</p>
<ul>
<li>Dua server Linux yang tidak dapat dihubungi dari Internet secara langsung karena berada di-balik NAT. Mereka akan menerima
perintah dan mengembalikan hasil eksekusi perintah tersebut.</li>
<li>Sebuah server Linux yang memberikan perintah jarak jauh. Server ini berada di jaringan publik yang sama dengan TURN server.</li>
<li>Sebuah TURN server siap pakai. Saya memilih untuk menggunakan <a href="https://github.com/coturn/coturn">coturn</a> pada latihan ini.</li>
</ul>
<p>Arsitektur aplikasi latihan tersebut terlihat seperti pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00108.png" alt="Arsitektur Percobaan" class="img-fluid rounded" /></p>
<p>Pada gambar di atas, saya menggunakan dua router berbeda untuk mensimulasikan Internet. Perangkat <code class="language-plaintext highlighter-rouge">server1</code> dan <code class="language-plaintext highlighter-rouge">server2</code>
dapat menghubungi <code class="language-plaintext highlighter-rouge">TURN_server</code> dan <code class="language-plaintext highlighter-rouge">remote</code>, namun tidak berlaku sebaliknya. Perangkat <code class="language-plaintext highlighter-rouge">remote</code> tidak bisa menghubungi langsung
<code class="language-plaintext highlighter-rouge">server1</code> dan <code class="language-plaintext highlighter-rouge">server2</code> yang berada di balik NAT. Ini adalah metode perlindungan yang umum dipakai untuk melindungi perangkat
dari serangan publik. Namun, berkat protokol TURN dan bantuan <code class="language-plaintext highlighter-rouge">TURN_server</code>, perangkat <code class="language-plaintext highlighter-rouge">remote</code> bisa melewati keterbatasan tersebut.</p>
<h4 id="turn-server">TURN Server</h4>
<p>Untuk melakukan instalasi coturn, saya akan memberikan perintah:</p>
<blockquote>
<p><strong>$</strong> <code>apt install coturn</code></p>
</blockquote>
<p>Setelah instalasi selesai, saya akan melakukan perubahan di <code class="language-plaintext highlighter-rouge">/etc/turnserver.conf</code>. Saya akan menambahkan <em>static credential</em>
sehingga tidak semua orang bisa menggunakan TURN server ini:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>user=turn:12345678
realm=latihan
</code></pre></div></div>
<p><em>Authentication</em> melalui <em>long-term credential</em> seperti yang tulis pada <a href="https://blog.jocki.me/network/2023/09/24/memakai-long-term-credential-di-stun">artikel sebelumnya</a> adalah persyaratan yang wajib
untuk menggunakan protokol TURN. Agar perubahannya efektif, saya akan menjalankan ulang coturn dengan perintah:</p>
<blockquote>
<p><strong>$</strong> <code>sudo systemctl restart coturn</code></p>
</blockquote>
<h4 id="turn-client">TURN Client</h4>
<p>Seperti pada kode program di artikel-artikel sebelumnya, untuk mulai memakai protokol TURN, saya bisa menyiapkan struktur data
yang dibutuhkan dengan menggunakan kode program seperti:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">TURNServerConnection</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Connection</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPConn</span>
<span class="n">receivedMessageQueue</span> <span class="k">chan</span> <span class="o">*</span><span class="n">Message</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">InitializeTURN</span><span class="p">(</span><span class="n">netInterface</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Establishing network..."</span><span class="p">)</span>
<span class="n">ip</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">GetLocalIP</span><span class="p">(</span><span class="n">netInterface</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to get local IP address: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">localAddr</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s:0"</span><span class="p">,</span> <span class="n">ip</span><span class="o">.</span><span class="n">String</span><span class="p">()))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to resolve local address: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">remoteAddr</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"10.20.30.40:3478"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to resolve remote address: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">DialUDP</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">localAddr</span><span class="p">,</span> <span class="n">remoteAddr</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to establish connection to TURN server: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">turnServerConnection</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">TURNServerConnection</span><span class="p">)</span>
<span class="n">turnServerConnection</span><span class="o">.</span><span class="n">Connection</span> <span class="o">=</span> <span class="n">conn</span>
<span class="n">turnServerConnection</span><span class="o">.</span><span class="n">receivedMessageQueue</span> <span class="o">=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="m">100</span><span class="p">)</span>
<span class="k">return</span> <span class="n">turnServerConnection</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Salah satu hal penting bagi TURN client adalah mempertahankan 5-Tuple yang sama sehingga tetap bisa menerima <em>packet</em> dari TURN server
setelah <em>UDP hole punching</em> di router. Oleh sebab itu, saya akan menggunakan sebuah <code class="language-plaintext highlighter-rouge">UDPConn</code> yang sama di seluruh komunikasi jaringan. Selain itu,
karena UDP yang bersifat <em>stateless</em>, saya menggunakan fitur <em>channel</em> di Go sebagai sebuah <em>buffer</em> FIFO untuk setiap <em>packet</em> STUN yang masuk. Saya
tidak bisa mengandalkan <em>packet</em> akan selalu dikirim dalam urutan yang sama persis sehingga saya perlu mencari <em>packet</em> yang
diharapkan berdasarkan <em>transaction id</em> dengan kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">WaitForReceivedMessage</span><span class="p">(</span><span class="n">transactionId</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span><span class="p">,</span> <span class="n">result</span> <span class="k">chan</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">{</span>
<span class="n">timeoutChannel</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">After</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
<span class="k">for</span> <span class="p">{</span>
<span class="k">select</span> <span class="p">{</span>
<span class="k">case</span> <span class="n">m</span> <span class="o">:=</span> <span class="o"><-</span><span class="n">turnServerConnection</span><span class="o">.</span><span class="n">receivedMessageQueue</span><span class="o">:</span>
<span class="k">if</span> <span class="n">m</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">TransactionId</span><span class="p">[</span><span class="m">0</span><span class="p">]</span> <span class="o">==</span> <span class="n">transactionId</span><span class="p">[</span><span class="m">0</span><span class="p">]</span> <span class="o">&&</span>
<span class="n">m</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">TransactionId</span><span class="p">[</span><span class="m">1</span><span class="p">]</span> <span class="o">==</span> <span class="n">transactionId</span><span class="p">[</span><span class="m">1</span><span class="p">]</span> <span class="o">&&</span>
<span class="n">m</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">TransactionId</span><span class="p">[</span><span class="m">2</span><span class="p">]</span> <span class="o">==</span> <span class="n">transactionId</span><span class="p">[</span><span class="m">2</span><span class="p">]</span> <span class="p">{</span>
<span class="n">result</span> <span class="o"><-</span> <span class="n">m</span>
<span class="k">return</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">turnServerConnection</span><span class="o">.</span><span class="n">receivedMessageQueue</span> <span class="o"><-</span> <span class="n">m</span>
<span class="p">}</span>
<span class="k">case</span> <span class="o"><-</span><span class="n">timeoutChannel</span><span class="o">:</span>
<span class="n">result</span> <span class="o"><-</span> <span class="no">nil</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya menambahkan sebuah <em>channel</em> baru dengan nama <code class="language-plaintext highlighter-rouge">timeoutChannel</code> sehingga <em>function</em> di atas
hanya akan menunggu hingga maksimal 5 detik. Hal ini karena pada protokol UDP, ada kemungkinan <em>packet</em> tidak akan pernah
sampai, sehingga saya tidak perlu terus menunggu. Pada kode program untuk <em>production</em>, saya perlu menambahkan bagian
yang mengulangi pengiriman pesan bila hal ini terjadi.</p>
<p>Tentu saja kode program di atas tidak akan bekerja karena belum ada kode program yang mengirim data ke <em>channel</em>
<code class="language-plaintext highlighter-rouge">turnServerConnection.receivedMessageQueue</code>. Untuk itu, saya bisa membaca <em>packet</em> UDP dari <code class="language-plaintext highlighter-rouge">UDPConn</code> dengan kode program
seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">StartReceiving</span><span class="p">()</span> <span class="p">{</span>
<span class="n">buffer</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">65536</span><span class="p">)</span>
<span class="n">conn</span> <span class="o">:=</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">Connection</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Listening on %s...</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">conn</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">())</span>
<span class="k">for</span> <span class="p">{</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">conn</span><span class="o">.</span><span class="n">SetDeadline</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Failed to set deadline: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="k">continue</span>
<span class="p">}</span>
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">ReadFromUDP</span><span class="p">(</span><span class="n">buffer</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">continue</span>
<span class="p">}</span>
<span class="n">message</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">NewStunResponse</span><span class="p">(</span><span class="n">buffer</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">continue</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Received new STUN message."</span><span class="p">)</span>
<span class="n">turnServerConnection</span><span class="o">.</span><span class="n">receivedMessageQueue</span> <span class="o"><-</span> <span class="n">message</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Kode program di atas memiliki sebuah <em>for loop</em> tak terhingga yang akan terus menerus membaca <em>packet</em> yang masuk
dan mengirimkannya ke <em>channel</em> <code class="language-plaintext highlighter-rouge">turnServer.receivedMessageQueue</code> bila seandainya <em>packet</em> tersebut adalah <em>packet</em> STUN
yang valid.</p>
<p>Untuk mengirim <em>packet</em> STUN dan menunggu <em>packet</em> respon-nya (berdasarkan <em>transaction id</em>), saya dapat membuat kode
program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">SendStunMessage</span><span class="p">(</span><span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">SendStunMessageImmediately</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to send message: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
<span class="k">go</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">WaitForReceivedMessage</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">TransactionId</span><span class="p">,</span> <span class="n">ch</span><span class="p">)</span>
<span class="n">response</span> <span class="o">:=</span> <span class="o"><-</span><span class="n">ch</span>
<span class="k">if</span> <span class="n">response</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to wait for response message: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">response</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">SendStunMessageImmediately</span><span class="p">(</span><span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
<span class="n">conn</span> <span class="o">:=</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">Connection</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">conn</span><span class="o">.</span><span class="n">SetReadDeadline</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">err</span>
<span class="p">}</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">())</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">err</span>
<span class="p">}</span>
<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Sekarang, setelah kode program untuk fasilitas STUN selesai dibuat, saya siap untuk menggunakan protokol TURN. Langkah pertama
untuk memakai TURN adalah mengirim pesan <code class="language-plaintext highlighter-rouge">Allocate</code> (<code class="language-plaintext highlighter-rouge">0x003</code>) ke TURN server. Pesan STUN ini wajib memiliki
atribut <code class="language-plaintext highlighter-rouge">REQUESTED-TRANSPORT</code> (<code class="language-plaintext highlighter-rouge">0x0019</code>) yang berisi kode protokol yang hendak dipakai. Saya akan menggunakan nilai <code class="language-plaintext highlighter-rouge">17</code>
untuk mewakili protokol UDP, seperti yang terlihat pada kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">NewAllocateRequest</span><span class="p">()</span> <span class="o">*</span><span class="n">Message</span> <span class="p">{</span>
<span class="k">return</span> <span class="o">&</span><span class="n">Message</span><span class="p">{</span>
<span class="n">Header</span><span class="o">:</span> <span class="n">MessageHeader</span><span class="p">{</span>
<span class="n">MessageType</span><span class="o">:</span> <span class="n">Allocate</span><span class="p">,</span>
<span class="n">MessageLength</span><span class="o">:</span> <span class="m">8</span><span class="p">,</span>
<span class="n">MagicCookie</span><span class="o">:</span> <span class="n">MagicCookie</span><span class="p">,</span>
<span class="n">TransactionId</span><span class="o">:</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span><span class="p">{</span><span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">()},</span>
<span class="p">},</span>
<span class="n">Attributes</span><span class="o">:</span> <span class="p">[]</span><span class="n">MessageAttribute</span><span class="p">{</span>
<span class="n">MessageAttribute</span><span class="p">{</span>
<span class="n">Type</span><span class="o">:</span> <span class="m">0x0019</span><span class="p">,</span>
<span class="n">Length</span><span class="o">:</span> <span class="m">4</span><span class="p">,</span>
<span class="n">Value</span><span class="o">:</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">{</span><span class="m">17</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">},</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Bila TURN server berhasil melakukan alokasi alamat transportasi bagi TURN client ini, program akan mendapatkan respon
sukses. <em>Packet</em> STUN untuk respon sukses harus memiliki atribut <code class="language-plaintext highlighter-rouge">XOR-RELAYED-ADDRESS</code>, <code class="language-plaintext highlighter-rouge">LIFETIME</code> dan <code class="language-plaintext highlighter-rouge">XOR-MAPPED-ADDRESS</code>.
Saya bisa membuat sebuah struktur baru untuk menampung hasil kembalian tersebut, seperti yang terlihat pada contoh
kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">TURNAllocation</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">RelayedAddress</span> <span class="o">*</span><span class="n">MappedAddressAttribute</span>
<span class="n">ClientAddress</span> <span class="o">*</span><span class="n">MappedAddressAttribute</span>
<span class="n">Lifetime</span> <span class="kt">uint32</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">allocation</span> <span class="n">TURNAllocation</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"relayed transport address = %s:%d, client address = %s:%d"</span><span class="p">,</span>
<span class="n">allocation</span><span class="o">.</span><span class="n">RelayedAddress</span><span class="o">.</span><span class="n">IP</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">allocation</span><span class="o">.</span><span class="n">RelayedAddress</span><span class="o">.</span><span class="n">Port</span><span class="p">,</span>
<span class="n">allocation</span><span class="o">.</span><span class="n">ClientAddress</span><span class="o">.</span><span class="n">IP</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">allocation</span><span class="o">.</span><span class="n">ClientAddress</span><span class="o">.</span><span class="n">Port</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">Allocate</span><span class="p">(</span><span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">TURNAllocation</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Sending allocation request..."</span><span class="p">)</span>
<span class="n">message</span> <span class="o">:=</span> <span class="n">NewAllocateRequest</span><span class="p">()</span>
<span class="n">response</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">turnServerConnection</span><span class="p">,</span> <span class="n">cred</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"error sending allocation request: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Allocation has been completed!</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
<span class="n">allocation</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">TURNAllocation</span><span class="p">)</span>
<span class="n">allocation</span><span class="o">.</span><span class="n">RelayedAddress</span> <span class="o">=</span> <span class="n">GetXorMappedAddressAttribute</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0016</span><span class="p">),</span> <span class="n">response</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">()[</span><span class="m">4</span><span class="o">:</span><span class="m">20</span><span class="p">])</span>
<span class="n">allocation</span><span class="o">.</span><span class="n">ClientAddress</span> <span class="o">=</span> <span class="n">GetXorMappedAddressAttribute</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0020</span><span class="p">),</span> <span class="n">response</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">()[</span><span class="m">4</span><span class="o">:</span><span class="m">20</span><span class="p">])</span>
<span class="n">allocation</span><span class="o">.</span><span class="n">Lifetime</span> <span class="o">=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x000D</span><span class="p">)</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span>
<span class="k">return</span> <span class="n">allocation</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Nilai dari <code class="language-plaintext highlighter-rouge">allocation.RelayedAddress</code> adalah alamat untuk <em>relayed transport address</em>. Pada arsitektur latihan ini, saya akan
mendapatkan nilai seperti <code class="language-plaintext highlighter-rouge">10.20.30.40:52726</code> dimana nilai <em>port</em>-nya akan acak tergantung pada apa
yang diberikan oleh NAT server. Bila <em>peer</em> ingin menghubungi <em>client</em> ini, ia hanya perlu mengirim pesan ke <em>relayed transport address</em>
tersebut (perhatikan bahwa IP-nya adalah IP NAT server).</p>
<p>Nilai <code class="language-plaintext highlighter-rouge">allocation.ClientAddress</code> adalah apa yang yang disebut sebagai <em>client reflexive transport address</em>. Ini adalah IP
yang berhubungan dengan TURN client bila dilihat dari sisi TURN server. Walaupun TURN client memilik IP lokal <code class="language-plaintext highlighter-rouge">192.168.1.100</code>,
nilai <code class="language-plaintext highlighter-rouge">allocation.ClientAddress</code> akan terlihat seperti <code class="language-plaintext highlighter-rouge">10.20.30.1:xxxx</code> karena yang dilihat oleh TURN server
dan yang berhubungan langsung dengan TURN server adalah IP router <code class="language-plaintext highlighter-rouge">internet</code> di <code class="language-plaintext highlighter-rouge">10.20.30.1</code>.</p>
<div class="alert alert-info" role="alert">
<strong>TIPS:</strong> Spesifikasi RFC 8656 tidak mengatur bagaimana informasi <em>relayed transport address</em> harus dilewatkan
ke pihak lain yang akan memakainya. Pada latihan sederhana ini, saya akan mengetikkan <em>relayed transport address</em> secara
manual. Namun, pada aplikasi yang lebih kompleks, saya dapat menggunakan protokol lain seperti Interactive Connectivity
Establishment (ICE), REST API, dan sebagainya.
</div>
<p><em>Relayed transport address</em> yang diberikan oleh TURN server tidak bersifat permanen. Ia hanya berlaku sesuai dengan nilai TTL
yang tertera di atribut <code class="language-plaintext highlighter-rouge">LIFETIME</code>. Bahkan bila <em>relayed transport address</em> tidak kadaluarsa, saya tetap perlu mengirim
<em>packet</em> secara berkala agar NAT <em>binding</em> yang sudah ada tidak dihapus oleh router. Agar bisa tetap menggunakan <em>relayed transport address</em>
yang sama, saya perlu mengirim operasi <code class="language-plaintext highlighter-rouge">Refresh</code> (<code class="language-plaintext highlighter-rouge">0x004</code>) ke TURN server sebelum nilai TTL dicapai. Untuk itu, saya bisa menggunakan
kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">NewRefreshRequest</span><span class="p">()</span> <span class="o">*</span><span class="n">Message</span> <span class="p">{</span>
<span class="k">return</span> <span class="o">&</span><span class="n">Message</span><span class="p">{</span>
<span class="n">Header</span><span class="o">:</span> <span class="n">MessageHeader</span><span class="p">{</span>
<span class="n">MessageType</span><span class="o">:</span> <span class="n">Refresh</span><span class="p">,</span>
<span class="n">MessageLength</span><span class="o">:</span> <span class="m">8</span><span class="p">,</span>
<span class="n">MagicCookie</span><span class="o">:</span> <span class="n">MagicCookie</span><span class="p">,</span>
<span class="n">TransactionId</span><span class="o">:</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span><span class="p">{</span><span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">()},</span>
<span class="p">},</span>
<span class="n">Attributes</span><span class="o">:</span> <span class="p">[]</span><span class="n">MessageAttribute</span><span class="p">{</span>
<span class="p">{</span>
<span class="n">Type</span><span class="o">:</span> <span class="m">0x8000</span><span class="p">,</span>
<span class="n">Length</span><span class="o">:</span> <span class="m">4</span><span class="p">,</span>
<span class="n">Value</span><span class="o">:</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">{</span><span class="m">1</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">},</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">Refresh</span><span class="p">(</span><span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">message</span> <span class="o">:=</span> <span class="n">NewRefreshRequest</span><span class="p">()</span>
<span class="n">response</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">turnServerConnection</span><span class="p">,</span> <span class="n">cred</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"error while sending allocation refresh request: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">response</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Satu-satunya atribut yang perlu saya berikan untuk operasi <code class="language-plaintext highlighter-rouge">Refresh</code> adalah <code class="language-plaintext highlighter-rouge">REQUESTED-ADDRESS-FAMILY</code> (<code class="language-plaintext highlighter-rouge">0x0017</code>). Nilainya
hanya bisa berupa <code class="language-plaintext highlighter-rouge">0x01</code> untuk alokasi IPv4 dan <code class="language-plaintext highlighter-rouge">0x02</code> untuk alokasi IPv6. Bila proses <em>refresh</em> sukses, saya akan mendapatkan
<em>packet</em> STUN kembalian yang didalamnya berisi atribut <code class="language-plaintext highlighter-rouge">LIFETIME</code> yang baru. Untuk mengerjakan <code class="language-plaintext highlighter-rouge">Refresh()</code> secara periodik,
misalnya saat 3/4 dari TTL sudah dicapai, saya dapat menggunakan kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">RefreshAllocation</span><span class="p">(</span><span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">,</span> <span class="n">lifetime</span> <span class="kt">uint32</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="p">{</span>
<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">lifetime</span><span class="o">*</span><span class="m">3</span><span class="o">/</span><span class="m">4</span><span class="p">)</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Refreshing allocation..."</span><span class="p">)</span>
<span class="n">response</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">Refresh</span><span class="p">(</span><span class="n">cred</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Error refreshing allocation: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">lifetime</span> <span class="o">=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x000D</span><span class="p">)</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Allocation refreshed. Timeout in %d seconds.</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">lifetime</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Langkah berikutnya yang perlu saya lakukan adalah mengirim operasi <code class="language-plaintext highlighter-rouge">CreatePermission</code> (<code class="language-plaintext highlighter-rouge">0x008</code>) untuk mengizinkan perangkat
<code class="language-plaintext highlighter-rouge">remote</code> dengan IP <code class="language-plaintext highlighter-rouge">10.20.30.50</code> mengirim pesan melalui TURN server. Operasi ini hanya membutuhkan atribut <code class="language-plaintext highlighter-rouge">XOR-PEER-ADDRESS</code>
(<code class="language-plaintext highlighter-rouge">0x0012</code>) yang berisi IP <em>peer</em> yang diizinkan. Saya bisa membuat <em>packet</em> STUN untuk operasi ini dengan kode program
seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">NewCreatePermission</span><span class="p">(</span><span class="n">peerIP</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="o">*</span><span class="n">Message</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">:=</span> <span class="o">&</span><span class="n">Message</span><span class="p">{</span>
<span class="n">Header</span><span class="o">:</span> <span class="n">MessageHeader</span><span class="p">{</span>
<span class="n">MessageType</span><span class="o">:</span> <span class="n">CreatePermission</span><span class="p">,</span>
<span class="n">MessageLength</span><span class="o">:</span> <span class="m">0</span><span class="p">,</span>
<span class="n">MagicCookie</span><span class="o">:</span> <span class="n">MagicCookie</span><span class="p">,</span>
<span class="n">TransactionId</span><span class="o">:</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span><span class="p">{</span><span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">()},</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="n">result</span><span class="o">.</span><span class="n">Attributes</span> <span class="o">=</span> <span class="p">[]</span><span class="n">MessageAttribute</span><span class="p">{</span>
<span class="n">CreateXorAddressAttribute</span><span class="p">(</span><span class="m">0x0012</span><span class="p">,</span> <span class="n">peerIP</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">result</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">()[</span><span class="m">4</span><span class="o">:</span><span class="m">20</span><span class="p">]),</span>
<span class="p">}</span>
<span class="n">result</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">MessageLength</span> <span class="o">=</span> <span class="kt">uint16</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">())</span> <span class="o">-</span> <span class="m">20</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Berdasarkan RFC 8656, <em>permission</em> akan kadaluarsa setelah 5 menit. Saya perlu kembali mengirim operasi <code class="language-plaintext highlighter-rouge">CreatePermission</code> sebelum
batas waktu 5 menit ini tercapai. Untuk itu, saya bisa menggunakan <code class="language-plaintext highlighter-rouge">time.Ticker</code> seperti pada kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">AddPermission</span><span class="p">(</span><span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">,</span> <span class="n">ip</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
<span class="n">ticker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="m">1</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Minute</span><span class="p">)</span>
<span class="n">action</span> <span class="o">:=</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Refreshing permission..."</span><span class="p">)</span>
<span class="n">ipClone</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">4</span><span class="p">)</span>
<span class="nb">copy</span><span class="p">(</span><span class="n">ipClone</span><span class="p">,</span> <span class="n">ip</span><span class="p">)</span>
<span class="n">message</span> <span class="o">:=</span> <span class="n">NewCreatePermission</span><span class="p">(</span><span class="n">ipClone</span><span class="p">)</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">turnServerConnection</span><span class="p">,</span> <span class="n">cred</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Error refreshing permission: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Permission refreshed"</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">action</span><span class="p">()</span>
<span class="k">for</span> <span class="p">{</span>
<span class="k">select</span> <span class="p">{</span>
<span class="k">case</span> <span class="o"><-</span><span class="n">ticker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
<span class="n">action</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Sebagai bagian yang paling terakhir, saya kini siap untuk menerima <em>packet</em> TURN yang berisi <code class="language-plaintext highlighter-rouge">Data Indication</code> (<code class="language-plaintext highlighter-rouge">0x0017</code>). Ini
adalah <em>packet</em> yang akan diterima oleh TURN client bila <em>peer</em> mengirim pesan ke <em>relayed transport address</em>. Pesan ini terdiri
atas 2 atribut: <code class="language-plaintext highlighter-rouge">XOR-PEER-ADDRESS</code> yang berisi <em>peer reflexive transport address</em> dan <code class="language-plaintext highlighter-rouge">DATA</code> yang berisi data yang dikirim oleh
<em>peer</em>. Saya bisa menggunakan nilai atribut <code class="language-plaintext highlighter-rouge">XOR-PEER-ADDRESS</code> untuk mengirim respon ke <em>peer</em> yang bersangkutan. Sebagai contoh,
saya dapat membuat kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">turnServerConnection</span> <span class="o">*</span><span class="n">TURNServerConnection</span><span class="p">)</span> <span class="n">ListenForData</span><span class="p">(</span><span class="n">handler</span> <span class="k">func</span><span class="p">([]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Listening for incoming data..."</span><span class="p">)</span>
<span class="k">for</span> <span class="p">{</span>
<span class="k">select</span> <span class="p">{</span>
<span class="k">case</span> <span class="n">m</span> <span class="o">:=</span> <span class="o"><-</span><span class="n">turnServerConnection</span><span class="o">.</span><span class="n">receivedMessageQueue</span><span class="o">:</span>
<span class="k">if</span> <span class="n">m</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">MessageType</span> <span class="o">==</span> <span class="m">0x0017</span> <span class="p">{</span>
<span class="n">peerAddress</span> <span class="o">:=</span> <span class="n">GetXorMappedAddressAttribute</span><span class="p">(</span><span class="n">m</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0012</span><span class="p">),</span> <span class="n">m</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">()[</span><span class="m">4</span><span class="o">:</span><span class="m">20</span><span class="p">])</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Received data indication from %s:%d..."</span><span class="p">,</span> <span class="n">peerAddress</span><span class="o">.</span><span class="n">IP</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">peerAddress</span><span class="o">.</span><span class="n">Port</span><span class="p">)</span>
<span class="n">data</span> <span class="o">:=</span> <span class="n">m</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0013</span><span class="p">)</span><span class="o">.</span><span class="n">Value</span>
<span class="n">response</span> <span class="o">:=</span> <span class="n">handler</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="n">sendIndicationResponse</span> <span class="o">:=</span> <span class="n">NewSendIndication</span><span class="p">(</span><span class="n">peerAddress</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">peerAddress</span><span class="o">.</span><span class="n">Port</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">SendStunMessageImmediately</span><span class="p">(</span><span class="n">sendIndicationResponse</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Failed to send data response: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">turnServerConnection</span><span class="o">.</span><span class="n">receivedMessageQueue</span> <span class="o"><-</span> <span class="n">m</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya menjalankan <em>for loop</em> tanpa henti yang akan menunggu datangnya <em>packet</em> <code class="language-plaintext highlighter-rouge">Data Indication</code>. Bila menemukannya,
ia akan melewatkan data yang diterima ke <em>function</em> <code class="language-plaintext highlighter-rouge">handler</code>. Hasil kembalian dari <em>function</em> <code class="language-plaintext highlighter-rouge">handler</code> kemudian dipakai untuk
membuat <em>packet</em> <code class="language-plaintext highlighter-rouge">Send Indication</code> (<code class="language-plaintext highlighter-rouge">0x0016</code>). Sama seperti <code class="language-plaintext highlighter-rouge">Data Indication</code>, pesan <code class="language-plaintext highlighter-rouge">Send Indication</code> hanya mengandung atribut <code class="language-plaintext highlighter-rouge">XOR-PEER-ADDRESS</code>
dan <code class="language-plaintext highlighter-rouge">DATA</code> (tanpa <em>long term authentication</em>). <code class="language-plaintext highlighter-rouge">Send Indication</code> digunakan agar TURN server dapat mengirim nilai yang tertera di atribut
<code class="language-plaintext highlighter-rouge">DATA</code> ke <em>peer</em> di alamat yang tertera di <code class="language-plaintext highlighter-rouge">XOR-PEER-ADDRESS</code>. Untuk membuat <em>packet</em> STUN-nya, saya bisa menggunakan kode program
seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">NewSendIndication</span><span class="p">(</span><span class="n">targetIP</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">targetPort</span> <span class="kt">uint16</span><span class="p">,</span> <span class="n">data</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="o">*</span><span class="n">Message</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">:=</span> <span class="o">&</span><span class="n">Message</span><span class="p">{</span>
<span class="n">Header</span><span class="o">:</span> <span class="n">MessageHeader</span><span class="p">{</span>
<span class="n">MessageType</span><span class="o">:</span> <span class="n">Send</span><span class="p">,</span>
<span class="n">MessageLength</span><span class="o">:</span> <span class="m">0</span><span class="p">,</span>
<span class="n">MagicCookie</span><span class="o">:</span> <span class="n">MagicCookie</span><span class="p">,</span>
<span class="n">TransactionId</span><span class="o">:</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span><span class="p">{</span><span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">()},</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="n">result</span><span class="o">.</span><span class="n">Attributes</span> <span class="o">=</span> <span class="p">[]</span><span class="n">MessageAttribute</span><span class="p">{</span>
<span class="n">CreateXorAddressAttribute</span><span class="p">(</span><span class="m">0x0012</span><span class="p">,</span> <span class="n">targetIP</span><span class="p">,</span> <span class="n">targetPort</span><span class="p">,</span> <span class="n">result</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">()[</span><span class="m">4</span><span class="o">:</span><span class="m">20</span><span class="p">]),</span>
<span class="n">CreateMessageAttribute</span><span class="p">(</span><span class="m">0x0013</span><span class="p">,</span> <span class="n">data</span><span class="p">),</span>
<span class="p">}</span>
<span class="n">result</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">MessageLength</span> <span class="o">=</span> <span class="kt">uint16</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">())</span> <span class="o">-</span> <span class="m">20</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Untuk implementasi <em>function</em> <code class="language-plaintext highlighter-rouge">handler</code>, saya bisa membuat sebuah <em>function</em> yang menerima perintah <em>shell</em>, mengerjakannya
melalui Bash dan mengembalikan <em>output</em> dari perintah tersebut seperti yang terlihat pada contoh kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">commandHandler</span><span class="p">(</span><span class="n">raw</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
<span class="n">command</span> <span class="o">:=</span> <span class="kt">string</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Executing command: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">command</span><span class="p">)</span>
<span class="n">output</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="s">"bash"</span><span class="p">,</span> <span class="s">"-c"</span><span class="p">,</span> <span class="n">command</span><span class="p">)</span><span class="o">.</span><span class="n">Output</span><span class="p">()</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">output</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Struktur kode program utama saya akan terlihat seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Welcome to TURN agent!"</span><span class="p">)</span>
<span class="n">cred</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">stun</span><span class="o">.</span><span class="n">LongTermCred</span><span class="p">)</span>
<span class="n">cred</span><span class="o">.</span><span class="n">Username</span> <span class="o">=</span> <span class="s">"turn"</span>
<span class="n">cred</span><span class="o">.</span><span class="n">Password</span> <span class="o">=</span> <span class="s">"12345678"</span>
<span class="n">turnServerConnection</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">stun</span><span class="o">.</span><span class="n">InitializeTURN</span><span class="p">(</span><span class="s">"ens5"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Failed to initialize network: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">go</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">StartReceiving</span><span class="p">()</span>
<span class="n">allocation</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">Allocate</span><span class="p">(</span><span class="n">cred</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Failed to allocate TURN relayed transport: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Allocation: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">allocation</span><span class="p">)</span>
<span class="k">go</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">RefreshAllocation</span><span class="p">(</span><span class="n">cred</span><span class="p">,</span> <span class="n">allocation</span><span class="o">.</span><span class="n">Lifetime</span><span class="p">)</span>
<span class="k">go</span> <span class="n">turnServerConnection</span><span class="o">.</span><span class="n">AddPermission</span><span class="p">(</span><span class="n">cred</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">{</span><span class="m">10</span><span class="p">,</span> <span class="m">20</span><span class="p">,</span> <span class="m">30</span><span class="p">,</span> <span class="m">50</span><span class="p">})</span>
<span class="n">turnServerConnection</span><span class="o">.</span><span class="n">ListenForData</span><span class="p">(</span><span class="n">commandHandler</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Bila saya menjalankan program ini, saya akan memperoleh hasil seperti berikut ini:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Welcome to TURN agent!
Establishing network...
Sending allocation request...
Listening on 192.168.1.100:51484...
Received new STUN message.
Received new STUN message.
Allocation has been completed!
Allocation: relayed transport address = 10.20.30.40:59914, client address = 10.20.30.1:51484
Listening for incoming data...
Refreshing permission...
Received new STUN message.
Permission refreshed
</code></pre></div></div>
<p>Sampai disini, program sudah siap untuk menerima pesan dari <em>peer</em> yang berupa server <code class="language-plaintext highlighter-rouge">remote</code>.</p>
<h4 id="remote-peer">Remote Peer</h4>
<p>Pada arsitektur latihan ini, server <code class="language-plaintext highlighter-rouge">remote</code> dengan IP <code class="language-plaintext highlighter-rouge">10.20.30.50</code> berada di jaringan publik yang sama yang terhubung
ke TURN server di <code class="language-plaintext highlighter-rouge">10.20.30.40</code>. Oleh sebab itu, saya tidak perlu membuat alokasi <em>relayed transport address</em> di TURN
server. Saya dapat langsung menghubungi TURN server untuk mengirim pesan ke TURN client, misalnya dengan menggunakan <code class="language-plaintext highlighter-rouge">nc</code>
seperti yang terlihat pada perintah berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>nc -u 10.20.30.40:59914</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>whoami
tester
pwd
/home/tester/turn-tunnel/sources
</code></pre></div> </div>
</blockquote>
<p>Terlihat bahwa walaupun saya menghubungi alamat <code class="language-plaintext highlighter-rouge">10.20.30.40</code>, sebenarnya saya berkomunikasi dengan TURN client di
alamat <code class="language-plaintext highlighter-rouge">192.168.1.100</code> yang sebelumnya tidak dapat saya hubungi dari publik. Selain itu, bila dilihat dari sisi
TURN client di <code class="language-plaintext highlighter-rouge">192.168.1.100</code>, seluruh komunikasi hanya terjadi ke TURN server di <code class="language-plaintext highlighter-rouge">10.20.30.40</code> tanpa melibatkan
<em>peer</em> <code class="language-plaintext highlighter-rouge">remote</code> sama sekali di <code class="language-plaintext highlighter-rouge">10.20.30.50</code>, seperti yang diperlihatkan oleh hasil <em>capture</em> di Wireshark pada
gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00110.png" alt="Hasil Capture Di Sisi TURN client" class="img-fluid rounded" /></p>Jocki HendryTraversal Using Relays around NAT (TURN) adalah sebuah protokol relay yang memungkinkan client berkomunikasi dengan peer yang tidak memiliki IP publik secara langsung (misalnya berada dibalik NAT). Protokol ini didefinisikan di RFC 8656. Komponen TURN terdiri atas TURN client dan TURN server. Komunikasi antara TURN client dengan peer selalu melalui TURN server yang berperan sebagai perantara. Oleh sebab itu, TURN server dan peer harus bisa saling berkomunikasi yang biasanya dilakukan dengan meletakkan TURN server pada jaringan publik.Memakai Long-Term Credential Di STUN2023-09-24T00:00:00+00:002023-09-24T00:00:00+00:00https://blog.jocki.me/network/2023/09/24/memakai-long-term-credential-di-stun<p><a href="https://www.ietf.org/rfc/rfc5389.txt">RFC 5389</a> mendefinisikan dua metode <em>authentication</em> untuk STUN: <em>short-term credential</em> dan
<em>long-term credential</em>. Metode <em>short-term credential</em> dipakai pada protokol seperti ICE sementara <em>long-term credential</em> merupakan
persyaratan untuk protokol TURN. Pada tulisan kali ini, saya akan mencoba menggunakan <em>long-term credential</em> di STUN.</p>
<p><em>Authentication</em> dengan menggunakan <em>long-term credential</em> yang umum dipakai di TURN biasanya dikaitkan pada 5-Tuple. Nilai 5-Tuple
terdiri atas kombinasi IP sumber, port sumber, IP tujuan, port tujuan, dan jenis protokol yang dipakai. Selama menggunakan 5-Tuple
yang sama, nilai <em>nonce</em> yang dibutuhkan untuk <em>authentication</em> tidak akan berubah (selain saat <em>server</em> mengirim kesalahan <code class="language-plaintext highlighter-rouge">438 Stale Nonce</code>
yang berarti <em>nonce</em> sudah kadualarsa). Untuk itu, saya membuat <em>struct</em> seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Tuple</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">LocalAddr</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span>
<span class="n">RemoteAddr</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Saya kemudian menambahkan sebuah <em>function</em> di <em>struct</em> tersebut untuk mengirim pesan STUN seperti yang saya lakukan di di <a href="https://blog.jocki.me/network/2023/09/06/apa-itu-protokol-stun">artikel sebelumnya</a>:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">tuple</span> <span class="o">*</span><span class="n">Tuple</span><span class="p">)</span> <span class="n">SendStunMessage</span><span class="p">(</span><span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">DialUDP</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">tuple</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">,</span> <span class="n">tuple</span><span class="o">.</span><span class="n">RemoteAddr</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to dial udp: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">SetReadDeadline</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to set read deadline: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">())</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to send message: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">1280</span><span class="p">)</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to read response: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">tuple</span><span class="o">.</span><span class="n">LocalAddr</span><span class="o">.</span><span class="n">Port</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
<span class="n">tuple</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">conn</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">()</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">response</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Bagian yang paling sulit ditebak dari 5-Tuple adalah <em>port</em> sumber. Nilai ini sebaiknya dinamis untuk menghindari konflik karena
sistem operasi bisa menggunakan berbagai <em>port</em> lokal untuk keperluan aplikasi lain. Untuk itu, saya bisa menggunakan nilai
seperti <code class="language-plaintext highlighter-rouge">192.168.1.100:0</code>. Nilai <em>port</em> <code class="language-plaintext highlighter-rouge">0</code> disini adalah kode bagi sistem operasi untuk memilih <em>port</em> apa saja yang bebas dan
tidak dipakai. Namun, setelah pilihan dibuat oleh sistem operasi, saya perlu mengingat nilai <em>port</em> yang dipilih karena
<em>authentication</em> di STUN/TURN terikat pada 5-Tuple. Oleh sebab itu, pada akhir <em>function</em> di atas, saya menulis kode program
seperti:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">...</span>
<span class="k">if</span> <span class="n">tuple</span><span class="o">.</span><span class="n">LocalAddr</span><span class="o">.</span><span class="n">Port</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
<span class="n">tuple</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">conn</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">()</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
<span class="p">}</span>
<span class="o">...</span>
</code></pre></div></div>
<p><em>Long-term credential</em> membutuhkan nilai <em>user</em>, <em>password</em>, dan <em>realm</em>. Selain itu, sebuah nilai <em>nonce</em> yang dikembalikan dari
sisi <em>server</em> juga perlu ikut di-simpan (sebagai <em>cookie</em>). Untuk menyimpan nilai-nilai tersebut, saya bisa mendefinisikan
sebuah <em>struct</em> seperti pada kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">LongTermCred</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Username</span> <span class="kt">string</span>
<span class="n">Password</span> <span class="kt">string</span>
<span class="n">Realm</span> <span class="p">[]</span><span class="kt">byte</span>
<span class="n">Nonce</span> <span class="p">[]</span><span class="kt">byte</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Sekarang, saya bisa mendefinisikan variabel dari <code class="language-plaintext highlighter-rouge">Tuple</code> dan <code class="language-plaintext highlighter-rouge">LongTermCred</code> seperti pada contoh kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tuple</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">stun</span><span class="o">.</span><span class="n">Tuple</span><span class="p">)</span>
<span class="n">tuple</span><span class="o">.</span><span class="n">LocalAddr</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"192.168.1.100:0"</span><span class="p">)</span>
<span class="n">tuple</span><span class="o">.</span><span class="n">RemoteAddr</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"10.20.30.40:3478"</span><span class="p">)</span>
<span class="n">cred</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">stun</span><span class="o">.</span><span class="n">LongTermCred</span><span class="p">)</span>
<span class="n">cred</span><span class="o">.</span><span class="n">Username</span> <span class="o">=</span> <span class="s">"jocki"</span>
<span class="n">cred</span><span class="o">.</span><span class="n">Password</span> <span class="o">=</span> <span class="s">"12345678"</span>
</code></pre></div></div>
<p>Langkah paling awal dalam <em>authentication</em> di STUN adalah mengirim <em>packet</em> seperti biasa tanpa <em>credential</em>. Bila <em>server</em> membutuhkan
<em>authentication</em>, saya akan memperoleh respon STUN yang mengandung atribut <code class="language-plaintext highlighter-rouge">ERROR-CODE</code> (<code class="language-plaintext highlighter-rouge">0x0009</code>). Bukan hanya itu, <em>server</em> juga
akan mengembalikan atribute <code class="language-plaintext highlighter-rouge">NONCE</code> (<code class="language-plaintext highlighter-rouge">0x0015</code>) dan <code class="language-plaintext highlighter-rouge">REALM</code> (<code class="language-plaintext highlighter-rouge">0x0014</code>) seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00109.png" alt="Respon Untuk Paket Pertama" class="img-fluid rounded" /></p>
<p>Saya perlu memastikan bahwah jenis kesalahan di atribut <code class="language-plaintext highlighter-rouge">ERROR-CODE</code> adalah <code class="language-plaintext highlighter-rouge">0x0401</code> yang berarti <code class="language-plaintext highlighter-rouge">Unauthorized</code>. Setelah yakin,
saya kemudian menyimpan nilai atribut <code class="language-plaintext highlighter-rouge">NONCE</code> dan <code class="language-plaintext highlighter-rouge">REALM</code> yang perlu saya pakai di <em>packet</em>-<em>packet</em> berikutnya hingga komunikasi
selesai. Untuk itu, saya bisa menulis kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">tuple</span> <span class="o">*</span><span class="n">Tuple</span><span class="p">,</span> <span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">,</span> <span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">raw</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">tuple</span><span class="o">.</span><span class="n">SendStunMessage</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"error sending authenticated message: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">stunResponse</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">NewStunResponse</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"error parsing authenticated message response: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">errorCodeAttribute</span> <span class="o">:=</span> <span class="n">stunResponse</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0009</span><span class="p">)</span>
<span class="k">if</span> <span class="n">errorCodeAttribute</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&&</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint16</span><span class="p">(</span><span class="n">errorCodeAttribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="m">4</span><span class="p">])</span> <span class="o">==</span> <span class="m">0x0401</span> <span class="p">{</span>
<span class="n">cred</span><span class="o">.</span><span class="n">storeCredential</span><span class="p">(</span><span class="n">stunResponse</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">stunResponse</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">)</span> <span class="n">storeCredential</span><span class="p">(</span><span class="n">stunResponse</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">{</span>
<span class="n">realmAttribute</span> <span class="o">:=</span> <span class="n">stunResponse</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0014</span><span class="p">)</span>
<span class="k">if</span> <span class="n">realmAttribute</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">cred</span><span class="o">.</span><span class="n">Realm</span> <span class="o">=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">realmAttribute</span><span class="o">.</span><span class="n">Value</span><span class="p">))</span>
<span class="nb">copy</span><span class="p">(</span><span class="n">cred</span><span class="o">.</span><span class="n">Realm</span><span class="p">,</span> <span class="n">realmAttribute</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">nonceAttribute</span> <span class="o">:=</span> <span class="n">stunResponse</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="m">0x0015</span><span class="p">)</span>
<span class="k">if</span> <span class="n">nonceAttribute</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">cred</span><span class="o">.</span><span class="n">Nonce</span> <span class="o">=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">nonceAttribute</span><span class="o">.</span><span class="n">Length</span><span class="p">)</span>
<span class="nb">copy</span><span class="p">(</span><span class="n">cred</span><span class="o">.</span><span class="n">Nonce</span><span class="p">,</span> <span class="n">nonceAttribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">0</span><span class="o">:</span><span class="n">nonceAttribute</span><span class="o">.</span><span class="n">Length</span><span class="p">])</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Setelah pesan pertama ditolak dengan error <code class="language-plaintext highlighter-rouge">0x0401</code>, saya perlu mengirim ulang pesan tersebut:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">tuple</span> <span class="o">*</span><span class="n">Tuple</span><span class="p">,</span> <span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">,</span> <span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="o">...</span>
<span class="k">if</span> <span class="n">errorCodeAttribute</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&&</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint16</span><span class="p">(</span><span class="n">errorCodeAttribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="m">4</span><span class="p">])</span> <span class="o">==</span> <span class="m">0x0401</span> <span class="p">{</span>
<span class="n">cred</span><span class="o">.</span><span class="n">storeCredential</span><span class="p">(</span><span class="n">stunResponse</span><span class="p">)</span>
<span class="k">return</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">tuple</span><span class="p">,</span> <span class="n">cred</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
<span class="p">}</span>
<span class="o">....</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Tentu saja bila saat pengiriman ulang, saya tidak melakukan apa-apa, pesan akan kembali ditolak dengan nilai <code class="language-plaintext highlighter-rouge">0x0401</code>. Yang
perlu saya lakukan adalah menambahkan atribut <code class="language-plaintext highlighter-rouge">USERNAME</code> (<code class="language-plaintext highlighter-rouge">0x0006</code>), <code class="language-plaintext highlighter-rouge">REALM</code> (<code class="language-plaintext highlighter-rouge">0x0014</code>) dan <code class="language-plaintext highlighter-rouge">NONCE</code> (<code class="language-plaintext highlighter-rouge">0x0015</code>) pada pesan
STUN tersebut. Karena seluruh nilai yang dibutuhkan sudah ada di <code class="language-plaintext highlighter-rouge">LongTermCred</code>, saya bisa menambahkan kode program seperti
berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">tuple</span> <span class="o">*</span><span class="n">Tuple</span><span class="p">,</span> <span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">,</span> <span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">cred</span><span class="o">.</span><span class="n">Nonce</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&&</span> <span class="n">cred</span><span class="o">.</span><span class="n">Realm</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">userAttribute</span> <span class="o">:=</span> <span class="n">CreateMessageAttribute</span><span class="p">(</span><span class="m">0x0006</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">cred</span><span class="o">.</span><span class="n">Username</span><span class="p">))</span>
<span class="n">realmAttribute</span> <span class="o">:=</span> <span class="n">CreateMessageAttribute</span><span class="p">(</span><span class="m">0x0014</span><span class="p">,</span> <span class="n">cred</span><span class="o">.</span><span class="n">Realm</span><span class="p">)</span>
<span class="n">nonceAttribute</span> <span class="o">:=</span> <span class="n">CreateMessageAttribute</span><span class="p">(</span><span class="m">0x0015</span><span class="p">,</span> <span class="n">cred</span><span class="o">.</span><span class="n">Nonce</span><span class="p">)</span>
<span class="n">message</span><span class="o">.</span><span class="n">Attributes</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">Attributes</span><span class="p">,</span> <span class="n">userAttribute</span><span class="p">,</span> <span class="n">realmAttribute</span><span class="p">,</span> <span class="n">nonceAttribute</span><span class="p">)</span>
<span class="n">message</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">MessageLength</span> <span class="o">+=</span> <span class="n">userAttribute</span><span class="o">.</span><span class="n">PaddedSize</span><span class="p">()</span> <span class="o">+</span> <span class="n">realmAttribute</span><span class="o">.</span><span class="n">PaddedSize</span><span class="p">()</span> <span class="o">+</span> <span class="n">nonceAttribute</span><span class="o">.</span><span class="n">PaddedSize</span><span class="p">()</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Hal yang perlu saya perhatikan saat membuat atribut di pesan STUN adalah ukuran nilai atribut harus kelipatan 4 bytes (32-bit).
Bila tidak genap kelipatan 4, saya perlu menambahkan <em>padding</em> yang nilainya bisa berupa apa saja. Sebagai contoh, nilai <em>username</em>
berupa <code class="language-plaintext highlighter-rouge">jocki</code> bukanlah kelipatan 4 bytes sehingga saya perlu menambahkan 3 bytes <em>padding</em> seperti <code class="language-plaintext highlighter-rouge">jocki000</code> agar menjadi
kelipatan 4 bytes. Secara kode program, saya dapat melakukannya dengan contoh seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">CreateMessageAttribute</span><span class="p">(</span><span class="n">attributeType</span> <span class="kt">uint16</span><span class="p">,</span> <span class="n">value</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="n">MessageAttribute</span> <span class="p">{</span>
<span class="n">paddedSize</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">if</span> <span class="n">paddedSize</span><span class="o">%</span><span class="m">4</span> <span class="o">!=</span> <span class="m">0</span> <span class="p">{</span>
<span class="n">paddedSize</span> <span class="o">+=</span> <span class="m">4</span> <span class="o">-</span> <span class="n">paddedSize</span><span class="o">%</span><span class="m">4</span>
<span class="p">}</span>
<span class="n">paddedValue</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">paddedSize</span><span class="p">)</span>
<span class="nb">copy</span><span class="p">(</span><span class="n">paddedValue</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="k">return</span> <span class="n">MessageAttribute</span><span class="p">{</span>
<span class="n">Type</span><span class="o">:</span> <span class="n">attributeType</span><span class="p">,</span>
<span class="n">Length</span><span class="o">:</span> <span class="kt">uint16</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">value</span><span class="p">)),</span>
<span class="n">Value</span><span class="o">:</span> <span class="n">paddedValue</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Sebagai bagian paling terakhir dan yang paling penting, saya juga perlu menambahkan atribut <code class="language-plaintext highlighter-rouge">MESSAGE-INTEGRITY</code> (<code class="language-plaintext highlighter-rouge">0x0008</code>). Nilai
ini merupakan nilai HMAC-SHA1 dari seluruh <em>packet</em> STUN tidak termasuk atribut <code class="language-plaintext highlighter-rouge">MESSAGE-INTEGRITY</code> tersebut. Nilai <em>key</em> yang
dipakai untuk kalkulasi HMAC-SHA1 adalah kombinasi dari:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>key = MD5(username ":" realm ":" password)
</code></pre></div></div>
<p>Salah hal penting yang perlu diperhatikan adalah <em>header</em> STUN mengandung informasi ukuran <em>packet</em> dalam <em>byte</em>. Saat melakukan
kalkulkasi HMAC-SHA1, nilai ini harus sudah memperhitungkan atribut <code class="language-plaintext highlighter-rouge">MESSAGE-INTEGRITY</code> sebesar 24 bytes, walaupun atribut tersebut
tidak ikut serta dalam kalkulasi.</p>
<p>Untuk melakukan kalkulasi HMAC-SHA1, saya dapat menggunakan kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">stunMessage</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="n">AddMessageIntegrity</span><span class="p">(</span><span class="n">key</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
<span class="n">stunMessage</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">MessageLength</span> <span class="o">+=</span> <span class="m">24</span>
<span class="n">md5Hash</span> <span class="o">:=</span> <span class="n">md5</span><span class="o">.</span><span class="n">New</span><span class="p">()</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">WriteString</span><span class="p">(</span><span class="n">md5Hash</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">hmacHash</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha1</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="n">md5Hash</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">))</span>
<span class="n">hmacHash</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">stunMessage</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">())</span>
<span class="n">stunMessage</span><span class="o">.</span><span class="n">Attributes</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">stunMessage</span><span class="o">.</span><span class="n">Attributes</span><span class="p">,</span> <span class="n">CreateMessageAttribute</span><span class="p">(</span><span class="m">0x0008</span><span class="p">,</span> <span class="n">hmacHash</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">)))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Saya kemudian melakukan perubahan pada <em>function</em> <code class="language-plaintext highlighter-rouge">SendWithAuth</code> menjadi seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">SendWithAuth</span><span class="p">(</span><span class="n">tuple</span> <span class="o">*</span><span class="n">Tuple</span><span class="p">,</span> <span class="n">cred</span> <span class="o">*</span><span class="n">LongTermCred</span><span class="p">,</span> <span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">cred</span><span class="o">.</span><span class="n">Nonce</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&&</span> <span class="n">cred</span><span class="o">.</span><span class="n">Realm</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="o">...</span>
<span class="n">message</span><span class="o">.</span><span class="n">AddMessageIntegrity</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s:%s:%s"</span><span class="p">,</span> <span class="n">cred</span><span class="o">.</span><span class="n">Username</span><span class="p">,</span> <span class="n">cred</span><span class="o">.</span><span class="n">Realm</span><span class="p">,</span> <span class="n">cred</span><span class="o">.</span><span class="n">Password</span><span class="p">))</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="k">return</span> <span class="n">stunResponse</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Setelah ini, bila <em>username</em> dan <em>password</em> yang saya pakai benar, saya tidak akan menemukan kesalahan <code class="language-plaintext highlighter-rouge">0x0401</code> saat
menerima respon dari <em>server</em> STUN.</p>Jocki HendryRFC 5389 mendefinisikan dua metode authentication untuk STUN: short-term credential dan long-term credential. Metode short-term credential dipakai pada protokol seperti ICE sementara long-term credential merupakan persyaratan untuk protokol TURN. Pada tulisan kali ini, saya akan mencoba menggunakan long-term credential di STUN.Apa Itu Protokol Session Traversal Utilities for NAT (STUN)?2023-09-06T00:00:00+00:002023-09-06T00:00:00+00:00https://blog.jocki.me/network/2023/09/06/apa-itu-protokol-stun<p>Salah satu istilah yang sering saya jumpai saat membuat kode program yang berhubungan dengan WebRTC adalah STUN. Session
Traversal Utilities for NAT (STUN) adalah protokol yang didefinisikan di <a href="https://www.ietf.org/rfc/rfc5389.txt">RFC 5389</a>.
STUN membantu mempermudah komunikasi dengan perangkat yang berada dibalik NAT yang tidak dapat dihubungi secara langsung
dari IP publik. Komponen STUN disebut sebagai STUN Agent yang terdiri atas STUN Client dan STUN Server.</p>
<h3 id="stun-lewat-go">STUN Lewat Go</h3>
<p>Sebagai latihan, saya akan membuat kode program Go yang berperan sebagai STUN Client dan mengirim pesan Binding Request ke
STUN Server publik milik Google di <code class="language-plaintext highlighter-rouge">stun.l.google.com</code>. Biasanya port yang dipakai untuk STUN adalah <code class="language-plaintext highlighter-rouge">UDP/3478</code>, namun
STUN Server gratis tersebut menggunakan port <code class="language-plaintext highlighter-rouge">UDP/19302</code>. Saya akan mulai dengan mendefinisikan struktur <em>packet</em> STUN seperti
pada kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Message</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Header</span> <span class="n">MessageHeader</span>
<span class="n">Attributes</span> <span class="p">[]</span><span class="n">MessageAttribute</span>
<span class="p">}</span>
<span class="k">type</span> <span class="n">MessageHeader</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">MessageType</span> <span class="kt">uint16</span>
<span class="n">MessageLength</span> <span class="kt">uint16</span>
<span class="n">MagicCookie</span> <span class="kt">uint32</span>
<span class="n">TransactionId</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span>
<span class="p">}</span>
<span class="k">type</span> <span class="n">MessageAttribute</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Type</span> <span class="kt">uint16</span>
<span class="n">Length</span> <span class="kt">uint16</span>
<span class="n">Value</span> <span class="p">[]</span><span class="kt">byte</span>
<span class="p">}</span>
<span class="k">type</span> <span class="n">MappedAddressAttribute</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Family</span> <span class="kt">uint8</span>
<span class="n">Port</span> <span class="kt">uint16</span>
<span class="n">IP</span> <span class="n">net</span><span class="o">.</span><span class="n">IP</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">MessageHeader</code> adalah sebuah struktur statis yang terdiri atas 20 bytes pertama dari <em>packet</em> STUN. Nilai <code class="language-plaintext highlighter-rouge">MessageType</code>
menunjukkan jenis pesan STUN yang diterima. Pada kode program sederhana ini, saya hanya akan menggunakan jenis pesan <code class="language-plaintext highlighter-rouge">BindingRequest</code>
dan <code class="language-plaintext highlighter-rouge">BindingResponse</code>:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="p">(</span>
<span class="n">BindingRequestType</span> <span class="o">=</span> <span class="m">0x0001</span>
<span class="n">BindingResponseType</span> <span class="o">=</span> <span class="m">0x0101</span>
<span class="p">)</span>
</code></pre></div></div>
<p>Nilai <code class="language-plaintext highlighter-rouge">MessageLength</code> menunjukkan isi dari pesan STUN (kosong atau beberapa <code class="language-plaintext highlighter-rouge">MessageAttribute</code>) dalam jumlah <em>byte</em> tidak
termasuk 20 bytes pertama (untuk <em>header</em>). Khusus untuk pesan <code class="language-plaintext highlighter-rouge">BindingRequest</code>, karena saya tidak perlu memakai
atribut, nilai dari <code class="language-plaintext highlighter-rouge">MessageLength</code> selalu <code class="language-plaintext highlighter-rouge">0</code>.</p>
<p>Nilai <code class="language-plaintext highlighter-rouge">MagicCookie</code> selalu berupa <code class="language-plaintext highlighter-rouge">0x2112A442</code>. Nilai ini dapat dipakai untuk memeriksa apakah <em>packet</em> STUN yang diterima
adalah bener <em>packet</em> STUN atau bukan.</p>
<p>Nilai <code class="language-plaintext highlighter-rouge">TransactionId</code> adalah sebuah angka pengenal unik dalam ukuran 12 bytes (96 bit). Pada saat mengirim pesan <code class="language-plaintext highlighter-rouge">BindingRequest</code>,
saya perlu mengisi nilai ini dengan sebuah nilai acak. Pada saat menerima pesan <code class="language-plaintext highlighter-rouge">BindingResponse</code>, saya perlu membandingkan
nilai <code class="language-plaintext highlighter-rouge">TransactionId</code> yang dterima apakah sama dengan nilai <code class="language-plaintext highlighter-rouge">TranscationId</code> saat dikirim. Ini untuk memastikan bahwa jawaban
yang diterima adalah jawaban untuk <em>request</em> yang saya berikan.</p>
<p>Untuk membuat sebuah pesan <code class="language-plaintext highlighter-rouge">BindingRequest</code>, saya dapat menggunakan kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">NewBindingRequest</span><span class="p">()</span> <span class="o">*</span><span class="n">Message</span> <span class="p">{</span>
<span class="k">return</span> <span class="o">&</span><span class="n">Message</span><span class="p">{</span>
<span class="n">Header</span><span class="o">:</span> <span class="n">MessageHeader</span><span class="p">{</span>
<span class="n">MessageType</span><span class="o">:</span> <span class="n">BindingRequestType</span><span class="p">,</span>
<span class="n">MessageLength</span><span class="o">:</span> <span class="m">0</span><span class="p">,</span>
<span class="n">MagicCookie</span><span class="o">:</span> <span class="n">MagicCookie</span><span class="p">,</span>
<span class="n">TransactionId</span><span class="o">:</span> <span class="p">[</span><span class="m">3</span><span class="p">]</span><span class="kt">uint32</span><span class="p">{</span><span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(),</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint32</span><span class="p">()},</span>
<span class="p">},</span>
<span class="n">Attributes</span><span class="o">:</span> <span class="no">nil</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Saya kemudian dapat mengirim pesan ini ke STUN server melalui koneksi UDP seperti pada <em>packet</em> lainnya, misalnya dengan kode program
seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">SendStunMessage</span><span class="p">(</span><span class="n">netInterface</span> <span class="kt">string</span><span class="p">,</span> <span class="n">stunServer</span> <span class="kt">string</span><span class="p">,</span> <span class="n">message</span> <span class="o">*</span><span class="n">Message</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">ip</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">GetLocalIP</span><span class="p">(</span><span class="n">netInterface</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to retrieve local ip for interface %s: %w"</span><span class="p">,</span> <span class="n">netInterface</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">localAddr</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">ip</span><span class="o">.</span><span class="n">String</span><span class="p">()</span><span class="o">+</span><span class="s">":0"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to resolve local addr %s: %w"</span><span class="p">,</span> <span class="n">ip</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">remoteAddr</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">stunServer</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to resolve remote addr %s: %w"</span><span class="p">,</span> <span class="n">stunServer</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">DialUDP</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">localAddr</span><span class="p">,</span> <span class="n">remoteAddr</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to dial udp: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">SetReadDeadline</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to set read deadline: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">GetBytes</span><span class="p">())</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to send message: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">response</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">1280</span><span class="p">)</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to read response: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">response</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Setelah mengirim <em>packet</em> STUN ke STUN Server, kode program di atas akan menunggu hingga maksimal 5 detik untuk mendapatkan
respon. STUN Server akan mengirim pesan <code class="language-plaintext highlighter-rouge">BindingResponse</code> yang berisi alamat NAT atau IP publik terakhir yang dilihat
oleh STUN server saat menerima pesan <code class="language-plaintext highlighter-rouge">BindingRequest</code>. Karena respon yang diterima berada dalam bentuk <code class="language-plaintext highlighter-rouge">[]byte</code>, saya dapat
menggunakan kode program seperti berikut ini untuk menerjemahkannya menjadi sebuah struktur <code class="language-plaintext highlighter-rouge">Message</code>:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">NewBindingResponse</span><span class="p">(</span><span class="n">raw</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">buf</span> <span class="o">:=</span> <span class="n">bytes</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
<span class="n">messageHeader</span> <span class="o">:=</span> <span class="n">MessageHeader</span><span class="p">{}</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">binary</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="p">,</span> <span class="o">&</span><span class="n">messageHeader</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to ready binary: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">messageHeader</span><span class="o">.</span><span class="n">MagicCookie</span> <span class="o">!=</span> <span class="n">MagicCookie</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid magic cookie %d"</span><span class="p">,</span> <span class="n">messageHeader</span><span class="o">.</span><span class="n">MagicCookie</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">message</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">Message</span><span class="p">)</span>
<span class="n">message</span><span class="o">.</span><span class="n">Header</span> <span class="o">=</span> <span class="n">messageHeader</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="kt">uint16</span><span class="p">(</span><span class="m">20</span><span class="p">);</span> <span class="n">i</span> <span class="o"><</span> <span class="kt">uint16</span><span class="p">(</span><span class="m">20</span><span class="p">)</span><span class="o">+</span><span class="n">messageHeader</span><span class="o">.</span><span class="n">MessageLength</span><span class="p">;</span> <span class="p">{</span>
<span class="n">attribute</span> <span class="o">:=</span> <span class="n">MessageAttribute</span><span class="p">{}</span>
<span class="n">attribute</span><span class="o">.</span><span class="n">Type</span> <span class="o">=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint16</span><span class="p">(</span><span class="n">raw</span><span class="p">[</span><span class="n">i</span> <span class="o">:</span> <span class="n">i</span><span class="o">+</span><span class="m">2</span><span class="p">])</span>
<span class="n">i</span> <span class="o">+=</span> <span class="m">2</span>
<span class="n">attribute</span><span class="o">.</span><span class="n">Length</span> <span class="o">=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint16</span><span class="p">(</span><span class="n">raw</span><span class="p">[</span><span class="n">i</span> <span class="o">:</span> <span class="n">i</span><span class="o">+</span><span class="m">2</span><span class="p">])</span>
<span class="n">i</span> <span class="o">+=</span> <span class="m">2</span>
<span class="n">attribute</span><span class="o">.</span><span class="n">Value</span> <span class="o">=</span> <span class="n">raw</span><span class="p">[</span><span class="n">i</span> <span class="o">:</span> <span class="n">i</span><span class="o">+</span><span class="n">attribute</span><span class="o">.</span><span class="n">Length</span><span class="p">]</span>
<span class="n">i</span> <span class="o">+=</span> <span class="n">attribute</span><span class="o">.</span><span class="n">Length</span>
<span class="n">message</span><span class="o">.</span><span class="n">Attributes</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">Attributes</span><span class="p">,</span> <span class="n">attribute</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">message</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya perlu menggunakan <em>looping</em> <code class="language-plaintext highlighter-rouge">for</code> karena sebuah <em>packet</em> STUN dapat berisi lebih dari satu
<code class="language-plaintext highlighter-rouge">MessageAttribute</code> dimana masing-masing <code class="language-plaintext highlighter-rouge">MessageAttribute</code> memiliki ukuran yang bervariasi tergantung pada nilai <code class="language-plaintext highlighter-rouge">Length</code>-nya. Setiap
<code class="language-plaintext highlighter-rouge">MessageAttribute</code> memiliki nilai <code class="language-plaintext highlighter-rouge">MessageType</code> yang menunjukkan jenis atribut (dan juga mendefinisikan struktur nilai dari
atribut tersebut). Khusus untuk <code class="language-plaintext highlighter-rouge">BindingResponse</code>, saya perlu mendapatkan nilai dari atribut <code class="language-plaintext highlighter-rouge">MappedAddressAttribute</code> dengan
nilai <code class="language-plaintext highlighter-rouge">0x0001</code> atau <code class="language-plaintext highlighter-rouge">XorMappedAddressAttributeType</code> dengan nilai <code class="language-plaintext highlighter-rouge">0x0020</code>:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="p">(</span>
<span class="n">MappedAddressAttributeType</span> <span class="o">=</span> <span class="m">0x0001</span>
<span class="n">XorMappedAddressAttributeType</span> <span class="o">=</span> <span class="m">0x0020</span>
<span class="p">)</span>
</code></pre></div></div>
<p>Untuk mengubah nilai <code class="language-plaintext highlighter-rouge">MessageAttribute.Value</code> menjadi sebuah <code class="language-plaintext highlighter-rouge">MappedAddressAttribute</code>, saya dapat menggunakan kode program
seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">GetMappedAddressAttribute</span><span class="p">(</span><span class="n">attribute</span> <span class="o">*</span><span class="n">MessageAttribute</span><span class="p">)</span> <span class="o">*</span><span class="n">MappedAddressAttribute</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">attribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">0</span><span class="p">]</span> <span class="o">!=</span> <span class="m">0</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
<span class="n">mappedAddressAttribute</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">MappedAddressAttribute</span><span class="p">)</span>
<span class="n">mappedAddressAttribute</span><span class="o">.</span><span class="n">Family</span> <span class="o">=</span> <span class="n">attribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">1</span><span class="p">]</span>
<span class="n">mappedAddressAttribute</span><span class="o">.</span><span class="n">Port</span> <span class="o">=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint16</span><span class="p">(</span><span class="n">attribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="m">4</span><span class="p">])</span>
<span class="n">mappedAddressAttribute</span><span class="o">.</span><span class="n">IP</span> <span class="o">=</span> <span class="n">attribute</span><span class="o">.</span><span class="n">Value</span><span class="p">[</span><span class="m">4</span><span class="o">:</span><span class="p">]</span>
<span class="k">return</span> <span class="n">mappedAddressAttribute</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Untuk <code class="language-plaintext highlighter-rouge">MappedAddressAttribute</code>, byte pertama selalu kosong. Saya dapat menggunakan fakta ini untuk memeriksa apakah atribut ini
valid atau tidak. Setelah itu, saya mulai dengan byte kedua yang berisi nilai untuk <code class="language-plaintext highlighter-rouge">Family</code>. Nilai <code class="language-plaintext highlighter-rouge">Family</code> berupa <code class="language-plaintext highlighter-rouge">0x01</code>
menunjukkan bawah ini adalah alamat IPv4 dan <code class="language-plaintext highlighter-rouge">0x02</code> untuk alamat IPv6. Berikutnya, byte ketiga dan keempat menjukkan nilai
<code class="language-plaintext highlighter-rouge">Port</code>. Sisanya adalah nilai alamat IP. Karena Go memiliki struktur <code class="language-plaintext highlighter-rouge">net.IP</code> untuk <code class="language-plaintext highlighter-rouge">[]byte</code>, saya menggunakan struktur
tersebut untuk nilai IP.</p>
<p>STUN server dari Google tidak mengembalikan atribut <code class="language-plaintext highlighter-rouge">MappedAddressAttribute</code> melainkan <code class="language-plaintext highlighter-rouge">XorMappedAddressAttribute</code>. Jenis
atribut ini hampir sama dengan nilai <code class="language-plaintext highlighter-rouge">MappedAddressAttribute</code>, hanya saja nilai <em>port</em> dan IP disamarkan melalui operasi
XOR terhadap <em>magic cookie</em> dan <em>transaction id</em>. Tujuan menyamarkan nilai tersebut adalah untuk mencegah perangkat jaringan
tertentu dalam memproses NAT secara tidak sengaja menulis ulang IP dan <em>port</em> yang hanya berupa informasi di <em>packet</em> STUN.</p>
<p>Untuk mendapatkan nilai <code class="language-plaintext highlighter-rouge">XorMappedAddressAttribute</code>, saya dapat menggunakan kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">GetXorMappedAddressAttribute</span><span class="p">(</span><span class="n">attribute</span> <span class="o">*</span><span class="n">MessageAttribute</span><span class="p">,</span> <span class="n">xorOperand</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="o">*</span><span class="n">MappedAddressAttribute</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">:=</span> <span class="n">GetMappedAddressAttribute</span><span class="p">(</span><span class="n">attribute</span><span class="p">)</span>
<span class="n">result</span><span class="o">.</span><span class="n">Port</span> <span class="o">^=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint16</span><span class="p">(</span><span class="n">xorOperand</span><span class="p">[</span><span class="m">0</span><span class="o">:</span><span class="m">2</span><span class="p">])</span>
<span class="n">xorAssignment</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">IP</span><span class="p">,</span> <span class="n">xorOperand</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">xorAssignment</span><span class="p">(</span><span class="n">a</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">b</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">);</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
<span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">^=</span> <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Saya dapat memanggil <em>function</em> di atas dengan menyertakan nilai byte <em>magic cookie</em> hingga <em>transaction id</em> seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">address</span> <span class="o">=</span> <span class="n">stun</span><span class="o">.</span><span class="n">GetXorMappedAddressAttribute</span><span class="p">(</span><span class="n">attribute</span><span class="p">,</span> <span class="n">response</span><span class="p">[</span><span class="m">4</span><span class="o">:</span><span class="m">20</span><span class="p">])</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Public IP Address is %s:%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">netInterface</span><span class="p">,</span> <span class="n">address</span><span class="o">.</span><span class="n">IP</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">address</span><span class="o">.</span><span class="n">Port</span><span class="p">)</span>
</code></pre></div></div>
<p>Sampai disini, saya sudah berhasil mendapatkan IP publik melalui protokol STUN.</p>
<h3 id="stun-lewat-web">STUN Lewat Web</h3>
<p>Selain dengan pemograman <em>low level</em> melalui Go, saya juga dapat menghubungi STUN Server melalui JavaScript di web browser. Hampir
semua browser modern mendukung WebRTC yang menggunakan protokol Interactive Connectivity Establishment (ICE). Protokol ICE
menggunakan STUN di tahap <em>client negotiation</em>. Dengan demikian, saya dapat memanfaatkan fase tersebut untuk mengetahui IP publik
dan menampilkannya di halaman web. Sebagai contoh, saya dapat membuat kode program seperti berikut ini:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">connection</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">RTCPeerConnection</span><span class="p">({</span>
<span class="na">iceServers</span><span class="p">:</span> <span class="p">[{</span>
<span class="na">urls</span><span class="p">:</span> <span class="dl">'</span><span class="s1">stun:stun.l.google.com:19302</span><span class="dl">'</span>
<span class="p">}]</span>
<span class="p">});</span>
<span class="nx">connection</span><span class="p">.</span><span class="nx">createDataChannel</span><span class="p">(</span><span class="dl">'</span><span class="s1">dummyChannel</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">connection</span><span class="p">.</span><span class="nx">createOffer</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">offer</span><span class="p">)</span> <span class="o">=></span> <span class="nx">connection</span><span class="p">.</span><span class="nx">setLocalDescription</span><span class="p">(</span><span class="nx">offer</span><span class="p">));</span>
</code></pre></div></div>
<p>Kode program di atas akan melakukan <em>binding request</em> ke STUN Server <code class="language-plaintext highlighter-rouge">stun.l.google.com:19302</code> untuk setiap perangkat jaringan lokal yang
dijumpai oleh web browser. Untuk mendapatkan informasi alamat IP publik, saya dapat menggunakan <em>event</em> <code class="language-plaintext highlighter-rouge">onicecandidate</code> seperti berikut ini:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">connection</span><span class="p">.</span><span class="nx">onicecandidate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">((</span><span class="nx">event</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">candidate</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">candidate</span><span class="p">.</span><span class="nx">candidate</span> <span class="o">===</span> <span class="dl">''</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">connection</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">candidate</span><span class="p">.</span><span class="nx">address</span><span class="p">}</span><span class="s2">:</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">candidate</span><span class="p">.</span><span class="nx">port</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Sebagai contoh, berikut ini adalah contoh hasil eksekusi JavaScript yang berusaha mendapatkan IP publik melalui WebRTC:</p>
<pre id="ip-output">
</pre>
<script>
const output = document.getElementById("ip-output");
output.textContent = "Starting...\n";
const connection = new RTCPeerConnection({
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
});
connection.createDataChannel('dummyChannel');
connection.createOffer()
.then((offer) => connection.setLocalDescription(offer))
.then(() => output.textContent += "Offer created...\n");
connection.onicecandidate = (event) => {
if ((event == null) || (event.candidate == null) || (event.candidate.candidate === '')) {
output.textContent += 'Done.\n';
connection.close();
return;
}
console.log(event.candidate);
output.textContent += `Received address ${event.candidate.address}:${event.candidate.port}\n`;
};
</script>Jocki HendrySalah satu istilah yang sering saya jumpai saat membuat kode program yang berhubungan dengan WebRTC adalah STUN. Session Traversal Utilities for NAT (STUN) adalah protokol yang didefinisikan di RFC 5389. STUN membantu mempermudah komunikasi dengan perangkat yang berada dibalik NAT yang tidak dapat dihubungi secara langsung dari IP publik. Komponen STUN disebut sebagai STUN Agent yang terdiri atas STUN Client dan STUN Server.Melakukan Binding Port Yang Sama Di Go Dengan SO_REUSEPORT2023-08-06T00:00:00+00:002023-08-06T00:00:00+00:00https://blog.jocki.me/network/2023/08/06/melakukan-binding-port-yang-sama-di-go<p>Sebuah <em>socket</em> di sistem operasi berbasis UNIX adalah kombinasi dari alamat IP sumber, port sumber, alamat IP tujuan
dan port tujuan. Pada umumnya, bila sebuah program ingin membuat <em>socket</em> baru, kombinasi dari ke-empat elemen tersebut
harus unik. Untuk membuktikannya, saya akan membuat sebuah program yang melakukan <em>binding</em> di port <code class="language-plaintext highlighter-rouge">12345/UDP</code> dan pada
saat yang bersamaan, juga mengirim pesan dari port <code class="language-plaintext highlighter-rouge">12345/UDP</code> ke alamat multicast <code class="language-plaintext highlighter-rouge">239.255.255.259</code> di port <code class="language-plaintext highlighter-rouge">3702/UDP</code>.
Contoh kode program Go-nya terlihat seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"bytes"</span>
<span class="s">"log"</span>
<span class="s">"net"</span>
<span class="s">"strings"</span>
<span class="s">"sync"</span>
<span class="p">)</span>
<span class="k">func</span> <span class="n">listen</span><span class="p">(</span><span class="n">wg</span> <span class="o">*</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">,</span> <span class="n">src</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span><span class="p">)</span> <span class="p">{</span>
<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
<span class="n">b</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">1024</span><span class="p">)</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ListenUDP</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">{</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">data</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">TrimSpace</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">b</span><span class="p">[</span><span class="o">:</span><span class="n">bytes</span><span class="o">.</span><span class="n">IndexByte</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="m">0</span><span class="p">)]))</span>
<span class="n">log</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Receiving: "</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="k">if</span> <span class="n">data</span> <span class="o">==</span> <span class="s">"exit"</span> <span class="p">{</span>
<span class="k">break</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">send</span><span class="p">(</span><span class="n">wg</span> <span class="o">*</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">,</span> <span class="n">src</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span><span class="p">,</span> <span class="n">dst</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span><span class="p">)</span> <span class="p">{</span>
<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">DialUDP</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"test_message"</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">src</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"127.0.0.1:12345"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">dst</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"239.255.255.250:3702"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">wg</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">)</span>
<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="k">go</span> <span class="n">listen</span><span class="p">(</span><span class="n">wg</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span>
<span class="k">go</span> <span class="n">send</span><span class="p">(</span><span class="n">wg</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span>
<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, terdapat <em>goroutine</em> <code class="language-plaintext highlighter-rouge">listen()</code> yang menerima koneksi UDP masuk di port <code class="language-plaintext highlighter-rouge">12345</code> dan <em>goroutine</em> <code class="language-plaintext highlighter-rouge">send()</code> yang
mengirim pesan UDP dari port <code class="language-plaintext highlighter-rouge">12345</code> ke port <code class="language-plaintext highlighter-rouge">3702</code> di alamat IP <em>multicast</em>. Terlihat bahwa kedua <em>goroutine</em> tersebut menggunakan
port <code class="language-plaintext highlighter-rouge">12345</code> yang sama. Apa yang terjadi saat program dijalankan? Saya akan menemukan pesan kesalahan seperti berikut ini:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2023/08/06 00:00:00 listen udp4 127.0.0.1:12345: bind: address already in use
</code></pre></div></div>
<p>Untuk mengatasi pesan kesalahan di atas, pada sistem operasi Ubuntu, saya perlu menggunakan <em>flag</em> <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code> saat
membuat <em>socket</em>. <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code> yang sudah ada sejak kernel Linux 3.9 memungkinkan sebuah aplikasi untuk menggunakan alamat IP
dan <em>port</em> yang sama berulang kali. Demi alasan keamanan, hal ini hanya bisa dilakukan bila <em>user id</em> (UID) program yang
menggunakan <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code> sama dengan UID program yang sudah menggunakan <em>socket</em> tersebut lebih
awal. Selain <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code>, juga ada pilihan <code class="language-plaintext highlighter-rouge">SO_REUSEADDR</code> yang lebih lama. Berbeda dengan <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code>, <code class="language-plaintext highlighter-rouge">SO_REUSEADDR</code>
tidak memiliki mekanisme untuk mencegah terjadinya <em>port hijacking</em>.</p>
<p>Walaupun bagian dari fitur resmi Go, flag <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code> merupakan fitur eksperimental yang bersifat <em>low level</em> dan tidak
bisa jalan di sistem operasi sehingga ia diletakkan di <em>package</em> terpisah. Untuk itu, saya perlu menambahkannya dengan
memberikan perintah seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>go get golang.org/x/sys/unix</code></p>
</blockquote>
<p>Setelah itu, saya kemudian mengubah kode program saya sehingga terlihat seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"bytes"</span>
<span class="s">"context"</span>
<span class="s">"golang.org/x/sys/unix"</span>
<span class="s">"log"</span>
<span class="s">"net"</span>
<span class="s">"strings"</span>
<span class="s">"sync"</span>
<span class="s">"syscall"</span>
<span class="p">)</span>
<span class="k">var</span> <span class="n">listenConfig</span> <span class="o">=</span> <span class="n">net</span><span class="o">.</span><span class="n">ListenConfig</span><span class="p">{</span>
<span class="n">Control</span><span class="o">:</span> <span class="k">func</span><span class="p">(</span><span class="n">network</span><span class="p">,</span> <span class="n">address</span> <span class="kt">string</span><span class="p">,</span> <span class="n">c</span> <span class="n">syscall</span><span class="o">.</span><span class="n">RawConn</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">c</span><span class="o">.</span><span class="n">Control</span><span class="p">(</span><span class="k">func</span><span class="p">(</span><span class="n">fd</span> <span class="kt">uintptr</span><span class="p">)</span> <span class="p">{</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">unix</span><span class="o">.</span><span class="n">SetsockoptInt</span><span class="p">(</span><span class="kt">int</span><span class="p">(</span><span class="n">fd</span><span class="p">),</span> <span class="n">unix</span><span class="o">.</span><span class="n">SOL_SOCKET</span><span class="p">,</span> <span class="n">unix</span><span class="o">.</span><span class="n">SO_REUSEPORT</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">listen</span><span class="p">(</span><span class="n">wg</span> <span class="o">*</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">,</span> <span class="n">src</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span><span class="p">)</span> <span class="p">{</span>
<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
<span class="n">b</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">1024</span><span class="p">)</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">listenConfig</span><span class="o">.</span><span class="n">ListenPacket</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="s">"udp4"</span><span class="p">,</span> <span class="n">src</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">{</span>
<span class="n">n</span><span class="p">,</span> <span class="n">addr</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">conn</span><span class="o">.</span><span class="n">ReadFrom</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">data</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">TrimSpace</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">b</span><span class="p">[</span><span class="o">:</span><span class="n">bytes</span><span class="o">.</span><span class="n">IndexByte</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="m">0</span><span class="p">)]))</span>
<span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Receiving %d bytes from %s => %s"</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">addr</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="k">if</span> <span class="n">data</span> <span class="o">==</span> <span class="s">"exit"</span> <span class="p">{</span>
<span class="k">break</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">send</span><span class="p">(</span><span class="n">wg</span> <span class="o">*</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">,</span> <span class="n">src</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span><span class="p">,</span> <span class="n">dst</span> <span class="o">*</span><span class="n">net</span><span class="o">.</span><span class="n">UDPAddr</span><span class="p">)</span> <span class="p">{</span>
<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
<span class="n">conn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">listenConfig</span><span class="o">.</span><span class="n">ListenPacket</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="s">"udp4"</span><span class="p">,</span> <span class="s">"127.0.0.1:12345"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">conn</span><span class="o">.</span><span class="n">WriteTo</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"test message"</span><span class="p">),</span> <span class="n">dst</span><span class="p">)</span>
<span class="n">log</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Written "</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="s">" bytes"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">src</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"127.0.0.1:12345"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">dst</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">ResolveUDPAddr</span><span class="p">(</span><span class="s">"udp4"</span><span class="p">,</span> <span class="s">"239.255.255.250:3702"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">wg</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">)</span>
<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="k">go</span> <span class="n">listen</span><span class="p">(</span><span class="n">wg</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span>
<span class="k">go</span> <span class="n">send</span><span class="p">(</span><span class="n">wg</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span>
<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya membuat <em>instance</em> dari <em>structure</em> <code class="language-plaintext highlighter-rouge">ListenConfig</code>. Saya dapat mendefinisikan kustomisasi
pada <em>socket</em> yang dipakai melalui nilai <code class="language-plaintext highlighter-rouge">Control</code>. Sebagai contoh, pada kode program di atas, saya memanggil
<code class="language-plaintext highlighter-rouge">unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)</code> untuk mengaktifkan <em>flag</em> <code class="language-plaintext highlighter-rouge">SO_REUSEPORT</code>. Setelah itu,
saya dapat memanggil <em>method</em> <code class="language-plaintext highlighter-rouge">ListenPacket()</code> milik <code class="language-plaintext highlighter-rouge">ListenConfig</code> untuk mempersiapkan <em>socket</em> dan mendapatkan <code class="language-plaintext highlighter-rouge">PacketConn</code>.
Saya kemudian menggunakan <em>method</em> <code class="language-plaintext highlighter-rouge">ReadFrom()</code> untuk menerima pesan atau <em>method</em> <code class="language-plaintext highlighter-rouge">WriteTo()</code> untuk mengirim pesan.</p>
<p>Saat kode program kembali dijalankan, saya tidak akan menemukan pesan kesalahan lagi. Selain itu, saya dapat mencoba menjalankan
program tersebut lebih dari sekali pada saat bersamaan (misalnya di Terminal baru tanpa menutup program sebelumnya). Walaupun
dijalankan lebih dari sekali dan menggunakan <em>port</em> yang sama, program tetap akan bekerja seperti biasanya.</p>Jocki HendrySebuah socket di sistem operasi berbasis UNIX adalah kombinasi dari alamat IP sumber, port sumber, alamat IP tujuan dan port tujuan. Pada umumnya, bila sebuah program ingin membuat socket baru, kombinasi dari ke-empat elemen tersebut harus unik. Untuk membuktikannya, saya akan membuat sebuah program yang melakukan binding di port 12345/UDP dan pada saat yang bersamaan, juga mengirim pesan dari port 12345/UDP ke alamat multicast 239.255.255.259 di port 3702/UDP. Contoh kode program Go-nya terlihat seperti berikut ini:Memakai IP Geolocation Di Suricata dan Kibana Tanpa Koneksi Internet2023-05-30T00:00:00+00:002023-05-30T00:00:00+00:00https://blog.jocki.me/network/2023/05/30/ip-geolocation-di-suricata-dan-kibana-secara-airgap<p>Pada suatu hari, saya melakukan instalasi Suricata dan Kibana di sebuah perangkat rumah untuk menjadikannya sebagai
monitor jaringan. Perangkat rumah ini hanya memiliki sebuah kartu jaringan yang terhubung ke SPAN port (mirror) tanpa
kemampuan melakukan koneksi Internet keluar. Walaupun demikian, instalasi Suricata, Elasticsearch, Kibana, dan Filebeat
berhasil dilakukan dengan lancar. Dashboard Kibana yang berisi daftar <em>events</em> dan <em>alerts</em> Suricata pun bekerja dengan
baik. Hanya saja beberapa visualiasi seperti <em>Top Source Countries</em> dan <em>Top Destination Countries</em> selalu memiliki
nilai yang kosong. Begitu juga dengan versi peta-nya, tidak ada data yang ditampilkan. Apa yang harus saya lakukan
agar visualisasi IP geolocation tersebut dapat bekerja dengan baik?</p>
<p>Langkah pertama yang saya lakukan adalah mendapatkan database GeoLite2 dari MaxMind. Informasi lebih lanjut mengenai
registrasi dan proses download dapat dilihat di <a href="https://dev.maxmind.com/geoip/geoip2/geolite2/">https://dev.maxmind.com/geoip/geoip2/geolite2/</a>. Bila tidak ingin
repot melakukan registrasi, saya juga dapat menggunakan kata kunci <code class="language-plaintext highlighter-rouge">GeoLite2-Country.mmdb</code>, <code class="language-plaintext highlighter-rouge">GeoLite2-City.mmdb</code> dan
<code class="language-plaintext highlighter-rouge">GeoLite2-ASN.mmdb</code> di Google untuk mencari file yang siap pakai untuk di-<em>download</em>. Setelah mendapatkan ketiga
file tersebut, saya meletakkannya ke lokasi instalasi Elasticsearch di <code class="language-plaintext highlighter-rouge">C:\Program Files\elasticsearch-8.7.1\config\ingest-geoip</code>.</p>
<p>Untuk memastikan IP geolocation bekerja dengan baik, saya membuka menu <strong>Management</strong>, <strong>Dev Tools</strong> di Kibana dan
memberikan <em>request</em> seperti berikut ini:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST _ingest/pipeline/_simulate
{
"pipeline": {
"processors": [
{
"geoip": {
"field": "location"
}
}
]
},
"docs": [
{
"_source": {
"location": "185.199.109.153"
}
}
]
}
</code></pre></div></div>
<p>Bila <em>GeoIP processor</em> bekerja dengan baik, saya akan mendapatkan <em>response</em> seperti berikut ini:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"docs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"doc"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"_index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"_index"</span><span class="p">,</span><span class="w">
</span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"_id"</span><span class="p">,</span><span class="w">
</span><span class="nl">"_version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"-3"</span><span class="p">,</span><span class="w">
</span><span class="nl">"_source"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="s2">"185.199.109.153"</span><span class="p">,</span><span class="w">
</span><span class="nl">"geoip"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"continent_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"North America"</span><span class="p">,</span><span class="w">
</span><span class="nl">"region_iso_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"US-CA"</span><span class="p">,</span><span class="w">
</span><span class="nl">"city_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"San Francisco"</span><span class="p">,</span><span class="w">
</span><span class="nl">"country_iso_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"US"</span><span class="p">,</span><span class="w">
</span><span class="nl">"country_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"United States"</span><span class="p">,</span><span class="w">
</span><span class="nl">"region_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"California"</span><span class="p">,</span><span class="w">
</span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"lon"</span><span class="p">:</span><span class="w"> </span><span class="mf">-122.3993</span><span class="p">,</span><span class="w">
</span><span class="nl">"lat"</span><span class="p">:</span><span class="w"> </span><span class="mf">37.7642</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"_ingest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-05-29T00:00:00.000Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Karena perangkat ini tidak memilikii koneksi Internet, saya dapat mematikan salah satu fitur <em>GeoIP processor</em> yang akan
berusaha memeriksa perbaharuan database GeoIP secara berkala. Saya dapat melakukannya dengan memberikan <em>request</em> seperti berikut ini:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUT _cluster/settings
{
"persistent": {
"ingest.geoip.downloader.enabled": false
}
}
</code></pre></div></div>
<p>Setelah ini, saya akan menemukan <em>property</em> seperti <code class="language-plaintext highlighter-rouge">destination.as.organization.name</code>, <code class="language-plaintext highlighter-rouge">destination.geo.location</code>,
<code class="language-plaintext highlighter-rouge">destination.geo.country_name</code> dan sebagainya untuk alamat IP publik seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00105.png" alt="Informasi IP Geolocation Di Event Suricata" class="img-fluid rounded" /></p>
<div class="alert alert-info" role="alert">
Proses penambahan informasi <em>geolocation</em> dilakukan oleh Elasticsearch saat menyimpan dokumen yang berisi alamat IP.
Tidak ada yang perlu dimodifikasi pada Suricata ataupun Filebeat.
</div>
<p>Sampai disini, visualisasi tabel seperti <em>Top Source Countries</em> dan <em>Top Destination Countries</em> sudah muncul dengan baik.<br />
Walaupun demikian, visualisasi peta masih belum bekerja dengan baik tanpa koneksi Internet. Sebagai contoh, saya
mendapatkan peta kosong seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00106.png" alt="Visualisasi peta tanpa Internet" class="img-fluid rounded" /></p>
<p>Hal ini karena untuk menampilkan peta, Kibana perlu melakukan koneksi ke Elastic Maps Service (EMS) di <code class="language-plaintext highlighter-rouge">tiles.maps.elastic.co</code>
dan <code class="language-plaintext highlighter-rouge">vector.maps.elastic.co</code>. Pada kondisi <em>airgap</em> (tanpa koneksi Internet), saya wajib menggunakan server peta yang dapat
diakses oleh mesin. Salah satu solusi dari Elastic Stack adalah Elastic Maps Server yang menyediakan EMS dalam bentuk instalasi
lokal (Docker). Sayangnya, Elastic Maps Server membutuhkan lisensi tersendiri yang tidak gratis.</p>
<p>Sebagai alternatif, saya akan memakai MapTiler Server yang dapat di-download secara gratis di <a href="https://www.maptiler.com/server/">https://www.maptiler.com/server/</a>.<br />
Walaupun server peta ini dapat dipakai secara gratis di komputer lokal tanpa koneksi Internet, saya tetap perlu men-<em>download</em>
salah satu dataset di <a href="https://data.maptiler.com/downloads/planet">https://data.maptiler.com/downloads/planet</a>. Hanya <em>OpenStreetMap vector tiles</em> yang dapat di-<em>download</em>
tanpa biaya dengan ukuran sekitar 70 GB. Karena ukurannya terlalu besar, saya akan mencoba menggunakan <em>test package</em>
yang dapat di-<em>download</em> di <a href="https://data.maptiler.com/maps">https://data.maptiler.com/maps</a> yang hanya berukuran 263 MB. Dataset ini sebenarnya hanya
untuk <em>demo</em> dan tidak berisi informasi yang lengkap untuk peta dunia.</p>
<p>Setelah melakukan instalasi MapTiler Server dan menjalankannya, saya dapat mengakses halaman administrasi peta di <a href="http://localhost:3650/admin">http://localhost:3650/admin</a>.<br />
Saya kemudian memilih menu <strong>Maps</strong> dan menambahkan peta <code class="language-plaintext highlighter-rouge">maptiler-server-map-styles-3.14.zip</code>. Setelah itu, saya
men-klik tombol <strong>Details</strong> pada peta <em>Satellite-Hybrid</em>. Disini saya akan menemukan URL yang dapat dipakai untuk mengakses
peta dalam format seperti <code class="language-plaintext highlighter-rouge">http://localhost:3650/api/maps/satellite-hybrid/{z}/{x}/{y}.jpg</code>. Saya dapat menggunakan URL ini
sebagai nilai <code class="language-plaintext highlighter-rouge">map.tilemap.url</code> di <code class="language-plaintext highlighter-rouge">kibana.yml</code>. Sebagai contoh, berikut ini penambahan yang saya lakukan pada file <code class="language-plaintext highlighter-rouge">kibana.yml</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>map.includeElasticMapsService: false
map.tilemap.url: "http://localhost:3650/api/maps/satellite-hybrid/{z}/{x}/{y}.jpg"
</code></pre></div></div>
<p>Setelah me-<em>restart</em> Kibana dan membuka kembali dashboard Suricata, visualisasi peta kini akan menggunakan <em>raster</em> dari MapTiler Server
seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00107.png" alt="Visualisasi peta di Kibana dengan MapTiler Server" class="img-fluid rounded" /></p>Jocki HendryPada suatu hari, saya melakukan instalasi Suricata dan Kibana di sebuah perangkat rumah untuk menjadikannya sebagai monitor jaringan. Perangkat rumah ini hanya memiliki sebuah kartu jaringan yang terhubung ke SPAN port (mirror) tanpa kemampuan melakukan koneksi Internet keluar. Walaupun demikian, instalasi Suricata, Elasticsearch, Kibana, dan Filebeat berhasil dilakukan dengan lancar. Dashboard Kibana yang berisi daftar events dan alerts Suricata pun bekerja dengan baik. Hanya saja beberapa visualiasi seperti Top Source Countries dan Top Destination Countries selalu memiliki nilai yang kosong. Begitu juga dengan versi peta-nya, tidak ada data yang ditampilkan. Apa yang harus saya lakukan agar visualisasi IP geolocation tersebut dapat bekerja dengan baik?Memakai Capture Filter di Suricata2023-05-28T00:00:00+00:002023-05-28T00:00:00+00:00https://blog.jocki.me/network/2023/05/28/memakai-capture-filter-di-suricata<p>Bagaimana caranya mengabaikan beberapa alamat IP supaya tidak diproses oleh Suricata? Salah satu solusinya adalah
dengan melakukan perubahan pada <em>rule</em> sehingga <em>alert</em> yang dihasilkan tidak menyertakan segala sesuatu yang berhubungan
dengan alamat IP tersebut. Kelemahan metode ini adalah Suricata tetap perlu mem-proses <em>packet</em> dari IP bersangkutan.
Yang terkena dampak perubahannya hanya pada <em>output</em> saja dimana <em>alert</em> yang telah diproses, jika ada, akan diabaikan.
Dengan demikian, metode ini tidaklah optimal dari sisi kinerja.</p>
<p>Cara yang lebih direkomendasikan adalah dengan menambahkan <em>capture filter BPF</em> saat menjalankan Suricata. Karena bekerja
pada <em>input</em>, <em>packet</em> yang telah disaring lewat <em>BPF rule</em> tidak akan diproses lebih lanjut lagi oleh Suricata. Dengan demikian,
tidak akan ada kemungkinan ada <em>alert</em> atau informasi lain yang berhubungan dengan <em>packet</em> tersebut.</p>
<p>Untuk menambahkan <em>capture filter</em>, saya dapat menjalankan Suricata dengan perintah seperti berikut ini:</p>
<blockquote>
<p><strong>C:\></strong> <code>suricata.exe -c suricata.yaml -i 10.0.0.2 not (host 10.0.0.6 or 10.0.0.7)</code></p>
</blockquote>
<p>Pada perintah di-atas, saya menggunakan ekspresi <code class="language-plaintext highlighter-rouge">not (host 10.0.0.6 or 10.0.0.7)</code> yang akan mengabaikan seluruh <em>packet</em> yang
berhubungan dengan IP <code class="language-plaintext highlighter-rouge">10.0.0.6</code> atau <code class="language-plaintext highlighter-rouge">10.0.0.7</code>.</p>
<p>Untuk mengabaikan sebuah <em>subnet</em>, seperti mengabaikan seluruh <em>packet</em> dari alamat <code class="language-plaintext highlighter-rouge">192.168.1.0</code> hingga <code class="language-plaintext highlighter-rouge">192.168.1.255</code>,
saya dapat menggunakan ekspresi seperti berikut ini:</p>
<blockquote>
<p><strong>C:\></strong> <code>suricata.exe -c suricata.yaml -i 10.0.0.2 not net 192.168.1.0/24</code></p>
</blockquote>
<p>Selain itu, saya juga bisa mengabaikan <em>packet</em> dari MAC address tertentu (yang bekerja di L2 <em>data link layer</em>) dengan
menggunakan ekspresi seperti berikut ini:</p>
<blockquote>
<p><strong>C:\></strong> <code>suricata.exe -c suricata.yaml -i 10.0.0.2 not ether host AABBCCDDEEFF</code></p>
</blockquote>
<p>dimana nilai <code class="language-plaintext highlighter-rouge">AABBCCDDEEFF</code> adalah MAC address dari perangkat jaringan yang ingin diabaikan.</p>Jocki HendryBagaimana caranya mengabaikan beberapa alamat IP supaya tidak diproses oleh Suricata? Salah satu solusinya adalah dengan melakukan perubahan pada rule sehingga alert yang dihasilkan tidak menyertakan segala sesuatu yang berhubungan dengan alamat IP tersebut. Kelemahan metode ini adalah Suricata tetap perlu mem-proses packet dari IP bersangkutan. Yang terkena dampak perubahannya hanya pada output saja dimana alert yang telah diproses, jika ada, akan diabaikan. Dengan demikian, metode ini tidaklah optimal dari sisi kinerja.Meningkatkan Keamanan Aplikasi Yang Menggunakan Firebase Authentication2023-04-23T00:00:00+00:002023-04-23T00:00:00+00:00https://blog.jocki.me/pemograman/2023/04/23/meningkatkan-keamanan-aplikasi-firebase-authentication<p>Pada suatu hari, saya diminta untuk membuat sebuah halaman login. Persyaratannya cukup sederhana: pengguna harus bisa
memasukkan email dan password, bila benar, pengguna akan diarahkan ke halaman utama. Saya pun segera menulis kode program
yang memanfaatkan Firebase Authentication. Dengan Firebase Authentication, bahkan pemula sekalipun bisa dengan
mudah membuat halaman login tanpa perlu mengkhawatirkan implementasi OAuth2, JWKS, database dan sejenisnya secara detail. Namun,
setelah halaman tersebut selesai dan bekerja sebagaimana seharusnya, karena masih ada sisa waktu, saya mulai berpikir: apakah ada hal
lain yang bisa saya lakukan untuk meningkatkan keamanan di halaman login tersebut? Pada tulisan ini, saya akan mengumpulkan
hasil pencarian saya yang berisi semua hal-hal tambahan yang bisa dilakukan untuk meningkatkan keamanan aplikasi yang menggunakan
Firebase Authentication. Semua informasi ini juga bisa dijumpai di dokumentasi Firebase Authentication.</p>
<hr />
<h3 id="mengaktifkan-email-enumeration-protection">Mengaktifkan Email Enumeration Protection</h3>
<p>Secara bawaan, bila email yang dimasukkan oleh pengguna belum terdaftar, Firebase Authentication akan mengembalikan respon dengan
pesan <code class="language-plaintext highlighter-rouge">EMAIL_NOT_FOUND</code>. Sementara itu, bila email sudah terdaftar namun password-nya salah, Firebase Authentication akan
mengembalikan pesan <code class="language-plaintext highlighter-rouge">INVALID_PASSWORD</code>. Walaupun ini sangat baik untuk <em>user experience</em> karena pengguna jadi tahu apa yang salah,
fasilitas ini dapat disalahgunakan oleh pihak yang berniat buruk untuk memeriksa apakah sebuah email adalah email yang terdaftar
di aplikasi yang saya buat. Setelah mengetahui apakah email valid, pihak dengan niat buruk tersebut bisa menindaklanjuti dengan
mengirim email phising atau memakai password yang pernah bocor dari email tersebut.</p>
<p>Untuk menghindari <em>email enumeration</em>, saya dapat memanggil API <a href="https://identitytoolkit.googleapis.com">https://identitytoolkit.googleapis.com</a> dengan menyertakan nilai
<code class="language-plaintext highlighter-rouge">true</code> pada <code class="language-plaintext highlighter-rouge">enable_improved_email_privacy</code>. Sebagai contoh, saya dapat melakukan pemanggilan seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>export PROJECT_ID=nama-proyek-gcp</code></p>
</blockquote>
<blockquote>
<p><strong>$</strong> <code>curl -i -X PATCH -d "{'email_privacy_config':{'enable_improved_email_privacy': 'true'}}" \<code><br />
<code>-H "Authorization: Bearer $(gcloud auth print-access-token --project $PROJECT_ID)" \<code><br />
<code>-H 'Content-Type: application/json' -H "X-Goog-User-Project: $PROJECT_ID" \<code><br />
<code>"https://identitytoolkit.googleapis.com/admin/v2/projects/$PROJECT_ID/config?updateMask=email_privacy_config"</code></code></code></code></code></code></code></p>
</blockquote>
<p>Bila tidak ada yang salah, saya akan memperoleh respon <code class="language-plaintext highlighter-rouge">200</code>. Setelah ini, bila menggunakan email yang tidak terdaftar maupun
terdaftar, bila password-nya salah, saya akan akan mendapatkan pesan kesalahan <code class="language-plaintext highlighter-rouge">INVALID_LOGIN_CREDENTIALS</code> yang sama.</p>
<hr />
<h3 id="mengaktifkan-multi-factor-authentication-mfa">Mengaktifkan Multi-Factor Authentication (MFA)</h3>
<p>Bila menggunakan Firebase Authentication bersamaan dengan Identity Platform, saya dapat menggunakan SMS sebagai perlindungan
tambahan bila password berhasil diketahui oleh pihak yang tidak bertanggung jawab. Untuk mengaktifkannya, saya dapat memilih
menu <strong>Sign-in method</strong> dan men-klik tombol <strong>Change</strong> pada bagian <em>SMS Multi-factor Authentication</em>. Saya kemudian mengaktifkan
tombol <strong>Enable</strong> seperti pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00101.png" alt="Mengaktifkan SMS Multi-Factor Authentication" class="img-fluid rounded" /></p>
<p>Bagian yang lumayan kompleks disini adalah kini saya perlu membuat halaman untuk melakukan registrasi nomor telepon dan juga
melakukan verifikasi kode SMS di halaman login bila pengguna memilih untuk mengaktifkan MFA.</p>
<p><br /></p>
<h4 id="pendaftaran-mfa">Pendaftaran MFA</h4>
<p>Sebelum melakukan registrasi nomor telepon, saya perlu memperbaharui token terlebih dahulu. Beberapa operasi sensitif di Firebase
Authentication seperti perubahan email juga mensyaratkan token yang segar dan akan gagal dengan pesan kesalahan<br />
<code class="language-plaintext highlighter-rouge">auth/requires-recent-login</code> bila usia token sudah terlalu lama (walaupun belum kadaluarsa). Sebagai contoh, saya membuat
halaman seperti pada gambar berikut ini dimana pengguna perlu memasukkan kembali password-nya:</p>
<p><img src="/assets/images/gambar_00102.png" alt="Halaman Untuk Pembaharuan Token" class="img-fluid rounded" /></p>
<p>Untuk memulai proses pembaharuan token, saya dapat memanggil <code class="language-plaintext highlighter-rouge">reauthenticateWithCredential()</code> seperti pada kode program berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">reauthenticate</span><span class="p">(</span><span class="nx">password</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">UserCredential</span><span class="o">></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getUser</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">user</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">EMPTY</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">from</span><span class="p">(</span><span class="nx">reauthenticateWithCredential</span><span class="p">(</span><span class="nx">user</span><span class="p">,</span> <span class="nx">EmailAuthProvider</span><span class="p">.</span><span class="nx">credential</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="o">!</span><span class="p">,</span> <span class="nx">password</span><span class="p">)));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Untuk mencegah penyalahgunaan, proses registrasi juga wajib diverifikasi melalui reCAPTCHA. Firebase Authentication sudah
menyediakan utilitas untuk ini sehingga saya tidak perlu menyiapkan reCAPTCHA secara manual. Saya bisa menggunakan <code class="language-plaintext highlighter-rouge">RecaptchaVerifier</code>
bawaan Firebase seperti pada contoh berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">createCaptchaVerifier</span><span class="p">():</span> <span class="nx">RecaptchaVerifier</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">parentDiv</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">recaptcha-container-parent</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">parentDiv</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Can't find element with id 'recaptcha-container-parent' in the page!`</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">recaptchaContainer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">div</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">parentDiv</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">recaptchaContainer</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">RecaptchaVerifier</span><span class="p">(</span><span class="nx">recaptchaContainer</span><span class="p">,</span> <span class="p">{</span><span class="na">size</span><span class="p">:</span> <span class="dl">'</span><span class="s1">invisible</span><span class="dl">'</span><span class="p">},</span> <span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada contoh di atas, saya menggunakan reCAPTCHA v2 (<em>invisible</em>). Pada metode ini, tidak ada <em>checkbox</em> <strong>I’m not a robot</strong> karena
reCAPTCHA akan berusaha sebisa mungkin melakukan pemeriksaan tanpa perlu interaksi dari pengguna. Walaupun demikian, pada trafik yang sangat mencurigakan,
reCAPTCHA tetap akan menampilkan pertanyaan untuk dijawab. Saya sempat menemukan permasalahan saat melakukan verifikasi reCAPTCHA
berulang kali pada halaman yang sama dengan pesan kesalahan <code class="language-plaintext highlighter-rouge">ReCAPTCHA has already been rendered in this element</code>. Untuk mengatasinya,
pada kode program di atas, saya terpaksa membuat ulang elemen <code class="language-plaintext highlighter-rouge"><div></code> untuk reCAPTCHA (dari <em>parent</em>-nya) setiap kali memakai <code class="language-plaintext highlighter-rouge">RecaptchaVerifier</code>.</p>
<p>Sekarang, saya bisa meminta pengguna untuk mengisi nomor telepon dan memulai proses pengiriman kode verifikasi SMS dengan
kode program seperti pada contoh berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">getMultiFactorUser</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getUser</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">user</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">User is empty!</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">multiFactor</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">getVerificationIdForEnrollment</span><span class="p">(</span><span class="nx">phoneNumber</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="kr">string</span><span class="o">></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Retrieving verification id for enrollment</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">from</span><span class="p">(</span><span class="k">new</span> <span class="nb">Promise</span><span class="o"><</span><span class="kr">string</span><span class="o">></span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">session</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">getMultiFactorUser</span><span class="p">().</span><span class="nx">getSession</span><span class="p">();</span>
<span class="kd">const</span> <span class="na">phoneInfoOptions</span><span class="p">:</span> <span class="nx">PhoneMultiFactorEnrollInfoOptions</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">phoneNumber</span><span class="p">,</span> <span class="nx">session</span> <span class="p">};</span>
<span class="kd">const</span> <span class="nx">phoneAuthProvider</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PhoneAuthProvider</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">verificationId</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">phoneAuthProvider</span><span class="p">.</span><span class="nx">verifyPhoneNumber</span><span class="p">(</span><span class="nx">phoneInfoOptions</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">createCaptchaVerifier</span><span class="p">());</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Verification id for enrollment has been retrieved!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">verificationId</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Kode program di atas akan mengembalikan sebuah <em>verification id</em> yang perlu saya pakai untuk verifikasi. Nilai ini perlu
dipadukan dengan kode verifikasi yang diterima oleh pengguna melalui SMS. Kombinasi dari <em>verification id</em> dan <em>verification code</em> yang
dimasukkan oleh pengguna akan dipakai untuk membuat <code class="language-plaintext highlighter-rouge">PhoneAuthCredential</code>. Untuk memeriksa apakah <em>verification code</em> sah, saya dapat
menggunakan <code class="language-plaintext highlighter-rouge">PhoneMultiFactorGenerator.assertion()</code> dengan melewatkan <code class="language-plaintext highlighter-rouge">PhoneAuthCredential</code> tersebut. <code class="language-plaintext highlighter-rouge">PhoneMultiFactorAssertion</code> yang sah ini
kemudian akan saya pakai untuk mendaftarkan nomor telepon melalui <code class="language-plaintext highlighter-rouge">MultiFactorUser.enroll()</code>. Agar lebih jelas, saya segera
menulis kode program seperti berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">enroll</span><span class="p">(</span><span class="nx">verificationId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">verificationCode</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="k">void</span><span class="o">></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Enrolling MFA code</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">from</span><span class="p">(</span><span class="k">new</span> <span class="nb">Promise</span><span class="o"><</span><span class="k">void</span><span class="o">></span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">cred</span> <span class="o">=</span> <span class="nx">PhoneAuthProvider</span><span class="p">.</span><span class="nx">credential</span><span class="p">(</span><span class="nx">verificationId</span><span class="p">,</span> <span class="nx">verificationCode</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">multiFactorAssertion</span> <span class="o">=</span> <span class="nx">PhoneMultiFactorGenerator</span><span class="p">.</span><span class="nx">assertion</span><span class="p">(</span><span class="nx">cred</span><span class="p">);</span>
<span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">getMultiFactorUser</span><span class="p">().</span><span class="nx">enroll</span><span class="p">(</span><span class="nx">multiFactorAssertion</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">User MFA has been enrolled!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">resolve</span><span class="p">();</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="p">}</span>
</code></pre></div></div>
<p><br /></p>
<h4 id="verifikasi-mfa-saat-login">Verifikasi MFA Saat Login</h4>
<p>Salah satu perubahan pada proses login adalah walaupun sudah memasukkan email dan password secara benar, proses login tetap
akan gagal dengan kesalahan <code class="language-plaintext highlighter-rouge">MultiFactorError</code>. Hal ini akan terjadi pada pengguna yang sebelumnya telah mengaktifkan MFA
dengan menggunakan <code class="language-plaintext highlighter-rouge">MultiFactorUser.enroll()</code>. Ini adalah kesalahan yang unik karena saya dapat menggunakan <code class="language-plaintext highlighter-rouge">MultiFactorError</code>
untuk melanjutkan proses login secara normal. Namun sebelumnya, saya perlu meminta Firebase Authentication
untuk mengirim kode verifikasi SMS ke pengguna terlebih dahulu dengan kode program seperti berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">getVerificationIdForLogin</span><span class="p">(</span><span class="nx">err</span><span class="p">:</span> <span class="nx">MultiFactorError</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">VerificationIdForLogin</span><span class="o">></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Retrieving verification id for login</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">from</span><span class="p">(</span><span class="k">new</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">VerificationIdForLogin</span><span class="o">></span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">resolver</span> <span class="o">=</span> <span class="nx">getMultiFactorResolver</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="kd">const</span> <span class="na">phoneInfoOptions</span><span class="p">:</span> <span class="nx">PhoneInfoOptions</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">multiFactorHint</span><span class="p">:</span> <span class="nx">resolver</span><span class="p">.</span><span class="nx">hints</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="na">session</span><span class="p">:</span> <span class="nx">resolver</span><span class="p">.</span><span class="nx">session</span><span class="p">,</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">phoneAuthProvider</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PhoneAuthProvider</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">verificationId</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">phoneAuthProvider</span><span class="p">.</span><span class="nx">verifyPhoneNumber</span><span class="p">(</span><span class="nx">phoneInfoOptions</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">createCaptchaVerifier</span><span class="p">());</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Verification id has been retrieved!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">resolve</span><span class="p">({</span><span class="nx">err</span><span class="p">,</span> <span class="nx">verificationId</span><span class="p">});</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Setelah meminta pengguna untuk mengisi kode verifikasi di SMS yang diterima, saya bisa menggunakan <code class="language-plaintext highlighter-rouge">MultiFactorResolver.resolveSignIn()</code>
untuk melanjutkan proses login tanpa perlu mengulang dari awal seperti pada contoh kode program berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">verify</span><span class="p">(</span><span class="nx">err</span><span class="p">:</span> <span class="nx">MultiFactorError</span><span class="p">,</span> <span class="nx">verificationId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">verificationCode</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">UserCredential</span><span class="o">></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Verifying MFA code</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">from</span><span class="p">(</span><span class="k">new</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">UserCredential</span><span class="o">></span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">cred</span> <span class="o">=</span> <span class="nx">PhoneAuthProvider</span><span class="p">.</span><span class="nx">credential</span><span class="p">(</span><span class="nx">verificationId</span><span class="p">,</span> <span class="nx">verificationCode</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">multiFactorAssertion</span> <span class="o">=</span> <span class="nx">PhoneMultiFactorGenerator</span><span class="p">.</span><span class="nx">assertion</span><span class="p">(</span><span class="nx">cred</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">resolver</span> <span class="o">=</span> <span class="nx">getMultiFactorResolver</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">credential</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">resolver</span><span class="p">.</span><span class="nx">resolveSignIn</span><span class="p">(</span><span class="nx">multiFactorAssertion</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">MFA code has been verified!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">credential</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Bila nilai <code class="language-plaintext highlighter-rouge">verificationCode</code> yang dimasukkan oleh pengguna benar (sesuai dengan yang diterima di SMS), proses login
akan sukses seperti biasanya.</p>
<p><br /></p>
<h4 id="unit-testing-yang-melibatkan-mfa">Unit Testing Yang Melibatkan MFA</h4>
<p>Untuk mencegah kuota SMS cepat habis, untuk pengujian secara lokal, saya dapat menggunakan Firebase Emulator. Bila Firebase
Authentication aktif di Firebase Emulator, setiap kali saya meminta kode verifikasi, tidak akan ada SMS yang dikirim ke nomor
telepon yang bersangkutan. Sebagai gantinya, kode verifikasi yang harus dimasukkan akan muncul di terminal yang menjalankan
Firebase Emulator.</p>
<p>Selain itu, pada <em>unit testing</em> yang otomatis, saya tetap dapat mengakses kode verifikasi melalui
URL <code class="language-plaintext highlighter-rouge">http://localhost:9099/emulator/v1/projects/demo-jocki/verificationCodes</code>. Sebagai contoh, berikut ini adalah contoh
skenario <em>unit test</em> Angular yang menguji alur pendaftaran MFA dan login MFA:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">findVerificationCode</span><span class="o"><</span><span class="nx">V</span><span class="o">></span><span class="p">(</span><span class="nx">verificationId</span><span class="p">:</span> <span class="nx">V</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="p">{</span><span class="na">id</span><span class="p">:</span> <span class="nx">V</span><span class="p">,</span> <span class="na">code</span><span class="p">:</span> <span class="kr">string</span><span class="p">}</span><span class="o">></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">vid</span> <span class="o">=</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">verificationId</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">)</span> <span class="p">?</span> <span class="nx">verificationId</span> <span class="p">:</span> <span class="p">(</span><span class="nx">verificationId</span> <span class="k">as</span> <span class="kr">any</span><span class="p">).</span><span class="nx">verificationId</span><span class="p">;</span>
<span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="kd">get</span><span class="o"><</span><span class="nx">VerificationCodes</span><span class="o">></span><span class="p">(</span><span class="dl">'</span><span class="s1">http://localhost:9099/emulator/v1/projects/demo-jocki/verificationCodes</span><span class="dl">'</span><span class="p">).</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">map</span><span class="p">(</span><span class="nx">v</span> <span class="o">=></span> <span class="p">({</span>
<span class="na">id</span><span class="p">:</span> <span class="nx">verificationId</span><span class="p">,</span>
<span class="na">code</span><span class="p">:</span> <span class="nx">v</span><span class="p">.</span><span class="nx">verificationCodes</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">v</span> <span class="o">=></span> <span class="nx">v</span><span class="p">.</span><span class="nx">sessionInfo</span> <span class="o">==</span> <span class="nx">vid</span><span class="p">)?.</span><span class="nx">code</span> <span class="o">??</span> <span class="dl">''</span><span class="p">,</span>
<span class="p">})),</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should return valid verification id for login through MultiFactorError</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">service</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="dl">'</span><span class="s1">owner@jocki.me</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">12345678</span><span class="dl">'</span><span class="p">).</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">mergeMap</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">service</span><span class="p">.</span><span class="nx">getVerificationIdForEnrollment</span><span class="p">(</span><span class="dl">'</span><span class="s1">+6212345678</span><span class="dl">'</span><span class="p">)),</span>
<span class="nx">mergeMap</span><span class="p">(</span><span class="nx">verificationId</span> <span class="o">=></span> <span class="nx">findVerificationCode</span><span class="p">(</span><span class="nx">verificationId</span><span class="p">)),</span>
<span class="nx">mergeMap</span><span class="p">(</span><span class="nx">v</span> <span class="o">=></span> <span class="nx">service</span><span class="p">.</span><span class="nx">enroll</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">id</span> <span class="k">as</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">v</span><span class="p">.</span><span class="nx">code</span><span class="p">)),</span>
<span class="nx">mergeMap</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">service</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="dl">'</span><span class="s1">owner@jocki.me</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">12345678</span><span class="dl">'</span><span class="p">)),</span>
<span class="nx">catchError</span><span class="p">(</span><span class="nx">err</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">code</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">AuthErrorCodes</span><span class="p">.</span><span class="nx">MFA_REQUIRED</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">service</span><span class="p">.</span><span class="nx">getVerificationIdForLogin</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">}),</span>
<span class="nx">mergeMap</span><span class="p">(</span><span class="nx">verificationId</span> <span class="o">=></span> <span class="nx">findVerificationCode</span><span class="o"><</span><span class="nx">VerificationIdForLogin</span><span class="o">></span><span class="p">(</span><span class="nx">verificationId</span> <span class="k">as</span> <span class="nx">VerificationIdForLogin</span><span class="p">)),</span>
<span class="nx">mergeMap</span><span class="p">(</span><span class="nx">v</span> <span class="o">=></span> <span class="nx">service</span><span class="p">.</span><span class="nx">verify</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">err</span><span class="p">,</span> <span class="nx">v</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">verificationId</span><span class="p">,</span> <span class="nx">v</span><span class="p">.</span><span class="nx">code</span><span class="p">))</span>
<span class="p">).</span><span class="nx">subscribe</span><span class="p">(</span> <span class="p">(</span><span class="nx">userCredential</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">userCredential</span><span class="p">.</span><span class="nx">operationType</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="dl">"</span><span class="s2">signIn</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">userCredential</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="dl">'</span><span class="s1">owner@jocki.me</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Bila ingin melakukan pengujian langsung ke server Firebase dan ingin menghemat kuota SMS, pada dashboard Firebase Authentication,
saya juga dapat menambahkan nomor telepon yang di-<em>hardcode</em> agar selalu mengirimkan kode verifikasi yang telah saya tentukan.</p>
<hr />
<p><br /></p>
<h4 id="mengatur-seberapa-lama-status-authentication-disimpan">Mengatur Seberapa Lama Status Authentication Disimpan</h4>
<p>Secara default, Firebase Authentication akan menyimpan status <em>authentication</em> di <em>browser</em> walaupun <em>browser</em> sudah ditutup
(selama pengguna tidak <em>logout</em> secara eksplisit). Ini akan membuat pengguna merasa lebih nyaman karena tidak perlu
sering kali <em>login</em> (termasuk memasukkan kode verifikasi SMS dan sebagainya). Namun, untuk aplikasi yang lebih sensitif, saya
dapat mengubah perilaku ini dengan memanggil <code class="language-plaintext highlighter-rouge">setPersistence()</code> dengan melewatkan salah satu <code class="language-plaintext highlighter-rouge">Persistence</code> berikut ini:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">browserLocalPersistence</code> untuk menyimpan status <em>authentication</em> hingga pengguna melakukan <em>logout</em> secara eksplisit.</li>
<li><code class="language-plaintext highlighter-rouge">browserSessionPersistence</code> untuk menyimpan status <em>authentication</em> hingga <em>tab</em> atau <em>browser</em> ditutup.</li>
<li><code class="language-plaintext highlighter-rouge">inMemoryPersistence</code> untuk tidak menyimpan status <em>authentication</em> sama sekali. Begitu halaman di-<em>refresh</em>, pengguna perlu login kembali.</li>
</ul>
<p>Sebagai contoh, saya dapat menggunakan <code class="language-plaintext highlighter-rouge">inMemoryPersistence</code> seperti pada kode program berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">browserSessionPersistence</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@angular/fire/auth</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">setPersistence</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@firebase/auth</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">login</span><span class="p">(</span><span class="nx">email</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">password</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">UserCredential</span><span class="o">></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">from</span><span class="p">(</span><span class="nx">setPersistence</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">,</span> <span class="nx">inMemoryPersistence</span><span class="p">)).</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">mergeMap</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">signInWithEmailAndPassword</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">,</span> <span class="nx">email</span><span class="p">,</span> <span class="nx">password</span><span class="p">))</span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Sekarang, seusai login, saya masih tetap bisa memakai aplikasi seperti biasanya. Namun begitu saya memperbaharui halaman (dengan
men-klik icon Refresh atau F5), saya akan diminta untuk login kembali. Walaupun paling merepotkan bagi pengguna, <code class="language-plaintext highlighter-rouge">inMemoryPersistence</code> adalah
konfigurasi yang paling aman karena <em>token</em> sama sekali tidak disimpan di-<em>browser</em> seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00103.png" alt="Tidak Ada Yang Disimpan Di Local Storage Maupun Session Storage" class="img-fluid rounded" /></p>
<p>Beberapa jenis serangan <em>session hijacking</em> menggunakan celah XSS untuk mengerjakan JavaScript yang kemudian membaca token
yang tersimpan di browser. Bila tidak ada <em>token</em> yang tersimpan di <em>browser</em> yang dapat dibaca melalui JavaScript, maka
teknik serangan seperti ini tidak akan bisa dipakai.</p>
<hr />
<p><br /></p>
<h4 id="mendeteksi-aktifitas-mencurigakan-berdasarkan-ip">Mendeteksi Aktifitas Mencurigakan Berdasarkan IP</h4>
<p>Bila terdapat perbedaan antara IP saat pengguna login dengan IP saat token dipakai, bisa jadi token tersebut telah dicuri. Firebase
Authentication mendukung pemeriksaan seperti ini secara <em>stateless</em> tanpa perlu database tersendiri dengan memanfaatkan <em>claim</em> di JWT.<br />
Seperti yang ditentukan oleh spesifikasi <a href="https://www.ietf.org/rfc/rfc7519.txt">RFC 7519</a>, sebuah token JWT dapat mengandung satu atau
lebih <em>claim</em>. Ada beberapa <em>claim</em> yang harus selalu ada di JWT seperti <code class="language-plaintext highlighter-rouge">iss</code>, <code class="language-plaintext highlighter-rouge">sub</code>, <code class="language-plaintext highlighter-rouge">aud</code>, <code class="language-plaintext highlighter-rouge">exp</code> dan sebagainya. Mereka disebut
sebagai <em>registered claims</em>. Selain itu, asalkan pihak yang berkomunikasi dapat saling memahami, JWT juga boleh mengandung <em>claim</em>
tambahan yang disebut sebagai <em>private claims</em>. Firebase Authentication menyebutnya sebagai <em>custom claims</em>.</p>
<p>Sebagai tambahan, selain <em>custom claims</em>, Firebase Authentication juga mendukung apa yang disebut <em>session claims</em>. Ini adalah <em>claim</em>
yang tidak akan disimpan secara permanen dan akan hilang saat <em>session</em> pengguna berakhir (misalnya saat token kadaluarsa atau
pengguna memilih <em>logout</em>). Nilai <em>session claims</em> hanya bisa ditambahkan oleh <em>blocking functions</em> <code class="language-plaintext highlighter-rouge">beforeSignIn</code>. Sebagai contoh,
saya akan membuat <em>blocking functions</em> seperti berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">beforeUserSignedIn</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">firebase-functions/v2/identity</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">before</span> <span class="o">=</span> <span class="nx">beforeUserSignedIn</span><span class="p">((</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="na">sessionClaims</span><span class="p">:</span> <span class="p">{</span>
<span class="na">signInIPAddress</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">ipAddress</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">};</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Sekarang, setiap kali token dihasilkan, akan ada informasi <code class="language-plaintext highlighter-rouge">signInIPAddress</code> yang berisi informasi IP klien yang
membuat token tersebut, seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00104.png" alt="Session Claim Baru Di JWT" class="img-fluid rounded" /></p>
<p>Selanjutnya, untuk mempermudah melakukan verifikasi alamat IP di seluruh <em>callable functions</em> yang ada, saya akan membuat
sebuah <em>currying function</em> seperti berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">getAuth</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">firebase-admin/auth</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">CallableRequest</span><span class="p">,</span> <span class="nx">HttpsError</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">firebase-functions/v2/https</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">verifyIP</span> <span class="o">=</span> <span class="o"><</span><span class="nx">T</span><span class="o">></span><span class="p">(</span><span class="nx">handler</span><span class="p">:</span> <span class="p">(</span><span class="nx">r</span><span class="p">:</span> <span class="nx">CallableRequest</span><span class="p">)</span> <span class="o">=></span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">T</span><span class="o">></span><span class="p">)</span> <span class="o">=></span> <span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">CallableRequest</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">tokenIP</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">auth</span><span class="p">?.</span><span class="nx">token</span><span class="p">?.</span><span class="nx">signInIPAddress</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">tokenIP</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">requestIP</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">rawRequest</span><span class="p">.</span><span class="nx">ip</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">uid</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">auth</span><span class="p">?.</span><span class="nx">uid</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Token IP [</span><span class="p">${</span><span class="nx">tokenIP</span><span class="p">}</span><span class="s2">] Request IP [</span><span class="p">${</span><span class="nx">requestIP</span><span class="p">}</span><span class="s2">] UID [</span><span class="p">${</span><span class="nx">uid</span><span class="p">}</span><span class="s2">]`</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">requestIP</span> <span class="o">&&</span> <span class="nx">uid</span> <span class="o">&&</span> <span class="p">(</span><span class="nx">tokenIP</span> <span class="o">!==</span> <span class="nx">requestIP</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Revoking refresh tokens for [</span><span class="p">${</span><span class="nx">uid</span><span class="p">}</span><span class="s2">]`</span><span class="p">);</span>
<span class="k">await</span> <span class="nx">getAuth</span><span class="p">().</span><span class="nx">revokeRefreshTokens</span><span class="p">(</span><span class="nx">uid</span><span class="p">);</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nx">HttpsError</span><span class="p">(</span><span class="dl">'</span><span class="s1">unauthenticated</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Unauthorized access</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">handler</span><span class="p">(</span><span class="nx">req</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Kode program di atas pada dasarnya akan membandingkan IP yang tercantum di JWT dengan IP dari <em>socket</em> saat pemanggilan
<em>function</em>. Bila terdapat perbedaan, seluruh token yang aktif untuk pengguna tersebut akan di-<em>revoke</em>. Ini berarti bukan
hanya pencuri token saja yang akan menemukan pesan kesalahan, pemilik akun yang sah juga akan dipaksa untuk login kembali.</p>
<p>Saya bisa menerapkan <em>currying function</em> tersebut ke seluruh <em>callable functions</em> yang ada seperti pada contoh berikut ini:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">api1</span> <span class="o">=</span> <span class="nx">onCall</span><span class="p">({</span><span class="na">maxInstances</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span> <span class="nx">verifyIP</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="p">...</span> <span class="p">}));</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">api2</span> <span class="o">=</span> <span class="nx">onCall</span><span class="p">({</span><span class="na">maxInstances</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span> <span class="nx">verifyIP</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="p">...</span> <span class="p">}));</span>
</code></pre></div></div>
<p>Untuk menguji apakah kode program yang saya buat bekerja dengan baik, saya akan melakukan langkah-langkah seperti berikut ini:</p>
<ol>
<li>Pada browser Chrome, buka tab <em>Network</em> untuk men-<em>capture</em> seluruh <em>request</em> dari browser.</li>
<li>Login sebagai user yang sah dan buka halaman yang melakukan pemanggilan <em>callable function</em>.</li>
<li>Pilih salah satu <em>request</em> yang mewakili pemanggilan <em>callable function</em>, pastikan terdapat header <code class="language-plaintext highlighter-rouge">authorization</code> pada <em>request</em> tersebut.<br />
Klik kanan pada <em>request</em> dan pilih <strong>Copy</strong>, <strong>Copy as cURL</strong>.</li>
<li>Pada dashboard GCP, buka Cloud Shell. Ini akan membuka terminal baru di mesin <em>remote</em> dengan IP yang dinamis. Tempelkan hasil pada
perintah sebelumnya dan tekan Enter untuk mengerjakan perintah cURL tersebut.</li>
<li>Pastikan untuk mendapatkan kembalian dengan status <code class="language-plaintext highlighter-rouge">401</code> dan pesan kesalahan seperti <code class="language-plaintext highlighter-rouge">{"error":{"message":"Unauthorized access","status":"UNAUTHENTICATED"}}</code>.</li>
</ol>
<div class="alert alert-warning" role="alert">
Melakukan troubleshooting dengan membagikan perintah cURL berisi <em>authorization header</em> bukanlah perilaku yang aman. Sebagai alternatif,
platform sebaiknya mendukung fasilitas <em>user impersonation</em> dimana pihak yang terlibat dalam troubleshooting
dapat menggunakan token milik mereka masing-masing untuk berperan sebagai pengguna. Dengan demikian, audit log akan mencatat token
milik developer beserta token milik pengguna yang disimulasikannya. Karena token hasil <em>user impersonation</em> ini berbeda dari
token yang biasa dipakai pengguna, platform juga dapat mengenali user mana saja yang sedang di-<em>impersonate</em> dengan mudah.
</div>
<p>Walaupun teknik perbandingan IP terlihat efektif, ia akan menimbulkan masalah bagi pengguna yang sedang dalam perjalanan atau
pengguna yang menggunakan koneksi telepor seluler dengan IP yang sangat dinamis. Mereka akan jadi lebih sering diminta untuk login
kembali. Untuk mengatasi hal ini, saya dapat meningkatkan kode program, misalnya dengan memeriksa apakah IP dari
dua negara yang berbeda, apakah IP bukan salah satu IP yang biasa dipakai selama 30 hari terakhir, apakah IP selalu berubah
dalam waktu singat, dan sebagainya.</p>Jocki HendryPada suatu hari, saya diminta untuk membuat sebuah halaman login. Persyaratannya cukup sederhana: pengguna harus bisa memasukkan email dan password, bila benar, pengguna akan diarahkan ke halaman utama. Saya pun segera menulis kode program yang memanfaatkan Firebase Authentication. Dengan Firebase Authentication, bahkan pemula sekalipun bisa dengan mudah membuat halaman login tanpa perlu mengkhawatirkan implementasi OAuth2, JWKS, database dan sejenisnya secara detail. Namun, setelah halaman tersebut selesai dan bekerja sebagaimana seharusnya, karena masih ada sisa waktu, saya mulai berpikir: apakah ada hal lain yang bisa saya lakukan untuk meningkatkan keamanan di halaman login tersebut? Pada tulisan ini, saya akan mengumpulkan hasil pencarian saya yang berisi semua hal-hal tambahan yang bisa dilakukan untuk meningkatkan keamanan aplikasi yang menggunakan Firebase Authentication. Semua informasi ini juga bisa dijumpai di dokumentasi Firebase Authentication.Melakukan Hashing Password Dengan Nonce di Sisi Client2023-03-07T00:00:00+00:002023-03-07T00:00:00+00:00https://blog.jocki.me/pemograman/2023/03/07/melakukan-hashing-password-dengan-nonce-di-sisi-client<p>Proses <em>hashing</em> untuk password di sisi <em>frontend</em> biasanya dilakukan supaya password tidak dikirimkan apa adanya (<em>plain text</em>) melalui
jaringan. Secara umum, proses ini tidak begitu meningkatkan keamanan password karena website modern sudah menggunakan HTTPS sehingga
password yang dikirim ke <em>backend</em> sudah ter-enkripsi. Proses <em>hashing</em> ini lebih berguna untuk serangan tertentu seperti MITM proxy dan
mencegah password tidak sengaja tersimpan di log <em>backend</em> (misalnya di server NGINX yang men-<em>log</em> seluruh <em>request body</em>).</p>
<p>Salah satu kriteria penting agar <em>hashing</em> efektif adalah hasil <em>hash</em> harus dinamis. Bila hasil <em>hash</em> selalu sama,
nilai <em>hash</em> secara tidak langsung akan menjadi password. Penyerang bisa dengan mudah menggunakan nilai <em>hash</em> untuk login
tanpa perlu tahu password yang sesungguhnya. Oleh sebab itu, pendekatan yang menggunakan metode enkripsi/dekripsi dimana password yang sama
akan menghasilkan hasil enkripsi yang sama, bukanlah solusi yang efektif. Pada tulisan ini, saya akan mencoba menambahkan <em>nonce</em> pada proses
<em>hashing</em> untuk menghindary <em>replay attack</em>.</p>
<h4 id="kondisi-awal">Kondisi Awal</h4>
<p>Sebagai latihan, saya akan membuat sebuah <em>backend</em> dari Go dengan menggunakan framework Gin. Ia akan menyediakan <em>endpoint</em>
untuk proses login dengan kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"github.com/gin-gonic/gin"</span>
<span class="s">"net/http"</span>
<span class="p">)</span>
<span class="k">const</span> <span class="n">EMAIL</span> <span class="o">=</span> <span class="s">"admin@jocki.me"</span>
<span class="k">const</span> <span class="n">PASSWORD</span> <span class="o">=</span> <span class="s">"password_rahasia_saya"</span>
<span class="k">type</span> <span class="n">Login</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Email</span> <span class="kt">string</span> <span class="s">`json:"email" binding:"required"`</span>
<span class="n">Password</span> <span class="kt">string</span> <span class="s">`json:"password" binding:"required"`</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">router</span> <span class="o">:=</span> <span class="n">gin</span><span class="o">.</span><span class="n">Default</span><span class="p">()</span>
<span class="n">router</span><span class="o">.</span><span class="n">SetTrustedProxies</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">LoadHTMLFiles</span><span class="p">(</span><span class="s">"index.html"</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">GET</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="s">"index.html"</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">POST</span><span class="p">(</span><span class="s">"/api/login"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="k">var</span> <span class="n">reqBody</span> <span class="n">Login</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">BindJSON</span><span class="p">(</span><span class="o">&</span><span class="n">reqBody</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusInternalServerError</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">()})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">reqBody</span><span class="o">.</span><span class="n">Email</span> <span class="o">!=</span> <span class="n">EMAIL</span> <span class="o">||</span> <span class="n">reqBody</span><span class="o">.</span><span class="n">Password</span> <span class="o">!=</span> <span class="n">PASSWORD</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusUnauthorized</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="s">"unauthorized"</span><span class="p">})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span>
<span class="s">"status"</span><span class="o">:</span> <span class="s">"ok"</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">Run</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program Go di atas, <em>endpoint</em> <code class="language-plaintext highlighter-rouge">/api/login</code> akan menerima <em>request</em> JSON yang mengandung <code class="language-plaintext highlighter-rouge">email</code> dan <code class="language-plaintext highlighter-rouge">password</code>. Ia kemudian
melakukan pemeriksaan untuk menentukan apakah email dan password-nya sesuai. Selain itu, kode program di atas juga melayani file
statis <code class="language-plaintext highlighter-rouge">index.html</code>. File ini akan mewakili <em>frontend</em> yang diakses langsung dari browser. Sebagai latihan,
saya akan membuat file <code class="language-plaintext highlighter-rouge">index.html</code> dengan isi seperti berikut ini:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">></span>
<span class="nt"><title></span>Latihan Hashing Password<span class="nt"></title></span>
<span class="nt"><style></span>
<span class="nt">form</span> <span class="p">{</span><span class="nl">display</span><span class="p">:</span> <span class="n">table</span><span class="p">}</span>
<span class="nt">form</span> <span class="nt">div</span> <span class="p">{</span><span class="nl">display</span><span class="p">:</span> <span class="nb">table-row</span><span class="p">}</span>
<span class="nt">label</span><span class="o">,</span> <span class="nt">input</span> <span class="p">{</span><span class="nl">display</span><span class="p">:</span> <span class="nb">table-cell</span><span class="p">;</span> <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">10px</span><span class="p">}</span>
<span class="nt">label</span> <span class="p">{</span><span class="nl">padding-right</span><span class="p">:</span> <span class="m">10px</span><span class="p">}</span>
<span class="nf">#output</span> <span class="p">{</span><span class="nl">white-space</span><span class="p">:</span> <span class="n">pre</span><span class="p">;</span> <span class="nl">font-family</span><span class="p">:</span> <span class="nb">monospace</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#eee</span><span class="p">;</span> <span class="nl">border</span><span class="p">:</span> <span class="m">#aaa</span> <span class="nb">solid</span> <span class="m">1px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">300px</span><span class="p">;</span> <span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span><span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Login<span class="nt"></h1></span>
<span class="nt"><div></span>
<span class="nt"><form</span> <span class="na">id=</span><span class="s">"form"</span><span class="nt">></span>
<span class="nt"><div></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"email"</span><span class="nt">></span>Email: <span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"email"</span> <span class="na">name=</span><span class="s">"email"</span> <span class="na">id=</span><span class="s">"email"</span> <span class="na">required</span><span class="nt">/></span>
<span class="nt"></div></span>
<span class="nt"><div></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"password"</span><span class="nt">></span>Password: <span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">name=</span><span class="s">"password"</span> <span class="na">id=</span><span class="s">"password"</span> <span class="na">required</span><span class="nt">/></span>
<span class="nt"></div></span>
<span class="nt"><div></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">id=</span><span class="s">"output"</span><span class="nt">></div></span>
<span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">form</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">output</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">output</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">submit</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">email</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">email</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">password</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">password</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Mengirim email [</span><span class="p">${</span><span class="nx">email</span><span class="p">}</span><span class="s2">] dan password [</span><span class="p">${</span><span class="nx">password</span><span class="p">}</span><span class="s2">] ke backend`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">}</span><span class="s2">/api/login`</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span><span class="nx">email</span><span class="p">,</span> <span class="nx">password</span><span class="p">}),</span>
<span class="p">});</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">+=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span> <span class="o">+</span> <span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">;</span>
<span class="p">});</span>
<span class="nt"></script></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>Halaman HTML di atas menggunakan JavaScript biasa tanpa <em>framework</em> (untuk dibuka dari browser modern). Ia menggunakan Fetch API untuk
memanggil REST API yang disediakan <em>backend</em> Go sebelumnya. Status hasil kembalian dari <em>backend</em> akan ditampilkan di halaman web
seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00095.png" alt="Halaman Web Awal" class="img-fluid rounded" /></p>
<p>Pada contoh eksekusi di atas, terlihat bahwa nilai <em>password</em> dikirim apa adanya ke <em>backend</em>. Salah satu potensi celah keamanan
disini adalah <em>password</em> tersebut tidak sengaja terekam di salah satu komponen <em>backend</em> seperti di log API gateway atau sejenisnya.
Bila seandainya <em>password</em> tidak pernah meninggalkan halaman HTML, maka tidak akan ada kebocoran kata sandi yang mungkin terjadi di sisi <em>backend</em>
(walaupun demikian, kebocoran dari sisi <em>frontend</em> seperti terekam di platform web <em>session replay</em> atau dibaca oleh <em>keylogger</em> di perangkat pengguna
tetap bisa saja terjadi!).</p>
<h4 id="melakukan-hashing-password-dengan-hmac-sha1">Melakukan Hashing Password Dengan HMAC-SHA1</h4>
<div class="alert alert-warning" role="alert">
Teknik yang ditunjukkan pada bagian ini tidak untuk dipakai melainkan hanya dibuat untuk mengilustrasikan proses <em>hashing</em> yang tidak aman!
</div>
<p>Satu langkah untuk meningkatkan keamanan adalah dengan melakukan proses <em>hashing</em> pada kode program di bagian sebelumnya. Sebagai contoh,
saya dapat menggunakan algoritma HMAC-SHA1 dengan sebuah <em>key</em> yang statis seperti pada kode program Go berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"crypto/hmac"</span>
<span class="s">"crypto/sha1"</span>
<span class="s">"github.com/gin-gonic/gin"</span>
<span class="s">"net/http"</span>
<span class="p">)</span>
<span class="k">const</span> <span class="n">EMAIL</span> <span class="o">=</span> <span class="s">"admin@jocki.me"</span>
<span class="k">const</span> <span class="n">PASSWORD</span> <span class="o">=</span> <span class="s">"password_rahasia_saya"</span>
<span class="k">const</span> <span class="n">STATIC_KEY</span> <span class="o">=</span> <span class="s">"sebuah_kunci_statis"</span>
<span class="k">type</span> <span class="n">Login</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Email</span> <span class="kt">string</span> <span class="s">`json:"email" binding:"required"`</span>
<span class="n">Password</span> <span class="p">[]</span><span class="kt">byte</span> <span class="s">`json:"password" binding:"required"`</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">hash</span><span class="p">(</span><span class="n">password</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
<span class="n">hmacSha1</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha1</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">STATIC_KEY</span><span class="p">))</span>
<span class="n">hmacSha1</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
<span class="k">return</span> <span class="n">hmacSha1</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">isValidUser</span><span class="p">(</span><span class="n">email</span> <span class="kt">string</span><span class="p">,</span> <span class="n">passwordHash</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">email</span> <span class="o">!=</span> <span class="n">EMAIL</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">false</span>
<span class="p">}</span>
<span class="n">expectedHash</span> <span class="o">:=</span> <span class="n">hash</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">PASSWORD</span><span class="p">))</span>
<span class="k">if</span> <span class="o">!</span><span class="n">hmac</span><span class="o">.</span><span class="n">Equal</span><span class="p">(</span><span class="n">passwordHash</span><span class="p">,</span> <span class="n">expectedHash</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">false</span>
<span class="p">}</span>
<span class="k">return</span> <span class="no">true</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">router</span> <span class="o">:=</span> <span class="n">gin</span><span class="o">.</span><span class="n">Default</span><span class="p">()</span>
<span class="n">router</span><span class="o">.</span><span class="n">SetTrustedProxies</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">LoadHTMLFiles</span><span class="p">(</span><span class="s">"index.html"</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">GET</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="s">"index.html"</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">POST</span><span class="p">(</span><span class="s">"/api/login"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="k">var</span> <span class="n">reqBody</span> <span class="n">Login</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">BindJSON</span><span class="p">(</span><span class="o">&</span><span class="n">reqBody</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusInternalServerError</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">()})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">isValidUser</span><span class="p">(</span><span class="n">reqBody</span><span class="o">.</span><span class="n">Email</span><span class="p">,</span> <span class="n">reqBody</span><span class="o">.</span><span class="n">Password</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span>
<span class="s">"status"</span><span class="o">:</span> <span class="s">"ok"</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusUnauthorized</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="s">"unauthorized"</span><span class="p">})</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">Run</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya melakukan kalkulasi HMAC SHA1 dengan menggunakan sebuah <code class="language-plaintext highlighter-rouge">STATIC_KEY</code> dengan nilai <code class="language-plaintext highlighter-rouge">"sebuah-kunci-statis"</code>. Agar
halaman web dapat menghasilkan kalkulasi HMAC SHA1 yang sama, saya juga perlu menggunakan nilai yang sama di halaman HTML, misalnya
seperti yang terlihat pada kode program berikut ini:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">form</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">output</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">output</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">submit</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">email</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">email</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">password</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">password</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">encoder</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">key</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">importKey</span><span class="p">(</span><span class="dl">'</span><span class="s1">raw</span><span class="dl">'</span><span class="p">,</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="dl">'</span><span class="s1">sebuah_kunci_statis</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="na">hash</span><span class="p">:</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">SHA-1</span><span class="dl">'</span><span class="p">}},</span> <span class="kc">false</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">sign</span><span class="dl">'</span><span class="p">]);</span>
<span class="kd">let</span> <span class="nx">hash</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">sign</span><span class="p">(</span><span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">password</span><span class="p">)));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Mengirim email [</span><span class="p">${</span><span class="nx">email</span><span class="p">}</span><span class="s2">] dan password [</span><span class="p">${</span><span class="nx">hash</span><span class="p">}</span><span class="s2">] ke backend`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">}</span><span class="s2">/api/login`</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span><span class="nx">email</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">hash</span><span class="p">)}),</span>
<span class="p">});</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">+=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span> <span class="o">+</span> <span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Pada kode program JavaScript di atas, saya menggunakan <code class="language-plaintext highlighter-rouge">crypto.subtle</code> yang merupakan bagian dari Web Cryptography API dan didukung oleh
browser modern. Saya menggunakan <code class="language-plaintext highlighter-rouge">importKey()</code> untuk menghasilkan sebuah <em>key</em> statis dengan nilai yang sama dengan yang saya pakai
di <em>backend</em>. Saya kemudian melewatkan <em>key</em> tersebut di <code class="language-plaintext highlighter-rouge">sign()</code> untuk menghasilkan <em>hash</em> dari <em>password</em> yang kemudian dikirim ke <em>backend</em>
sebagai <em>array</em> di JSON <em>request body</em>.</p>
<p>Sebagai contoh, bila saya membuka halaman ini dan mengirim <em>password</em>, saya akan memperoleh hasil seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00098.png" alt="Hash Statis Dengan HMAC SHA1" class="img-fluid rounded" /></p>
<p>Walaupun <em>password</em> kini tidak terlihat lagi, teknik ini sama sekali tidak meningkatkan keamanan. Hal ini disebabkan oleh hasil <em>hash</em> yang
selalu menghasilkan nilai yang sama (statis). Penyerang yang berhasil melihat nilai <em>hash</em> tetap dapat mengirim <em>hash</em> tersebut kapan saja
untuk masuk ke dalam website! Dengan kata lain, nilai <em>hash</em> perannya tidak jauh berbeda dari nilai <em>password</em> (yang perlu dilindungi).</p>
<h4 id="menambahkan-nonce-angka-pada-proses-hashing">Menambahkan Nonce Angka Pada Proses Hashing</h4>
<p>Agar proses <em>hashing</em> aman, saya dapat menambahkan <em>nonce</em> yang berupa angka acak. Dengan demikian, setiap <em>request</em> dengan <em>nonce</em> yang
berbeda akan menghasilkan <em>hash</em> yang berbeda namun tetap dapat diverifikasi oleh <em>backend</em>. Sebagai contoh, saya akan mengubah kode
program Go yang ada menjadi seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"crypto/hmac"</span>
<span class="s">"crypto/sha1"</span>
<span class="s">"encoding/binary"</span>
<span class="s">"fmt"</span>
<span class="s">"github.com/gin-contrib/sessions"</span>
<span class="s">"github.com/gin-contrib/sessions/cookie"</span>
<span class="s">"github.com/gin-gonic/gin"</span>
<span class="s">"math/rand"</span>
<span class="s">"net/http"</span>
<span class="s">"strconv"</span>
<span class="p">)</span>
<span class="k">const</span> <span class="n">EMAIL</span> <span class="o">=</span> <span class="s">"admin@jocki.me"</span>
<span class="k">const</span> <span class="n">PASSWORD</span> <span class="o">=</span> <span class="s">"password_rahasia_saya"</span>
<span class="k">type</span> <span class="n">Login</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Email</span> <span class="kt">string</span> <span class="s">`json:"email" binding:"required"`</span>
<span class="n">Password</span> <span class="p">[]</span><span class="kt">byte</span> <span class="s">`json:"password" binding:"required"`</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">hash</span><span class="p">(</span><span class="n">password</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">nonce</span> <span class="kt">uint64</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
<span class="n">hmacSha1</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha1</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="n">value</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">8</span><span class="p">)</span>
<span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">PutUint64</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">nonce</span><span class="p">)</span>
<span class="n">hmacSha1</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">return</span> <span class="n">hmacSha1</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">isValidUser</span><span class="p">(</span><span class="n">email</span> <span class="kt">string</span><span class="p">,</span> <span class="n">passwordHash</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">nonce</span> <span class="kt">uint64</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">email</span> <span class="o">!=</span> <span class="n">EMAIL</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">false</span>
<span class="p">}</span>
<span class="n">expectedHash</span> <span class="o">:=</span> <span class="n">hash</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">PASSWORD</span><span class="p">),</span> <span class="n">nonce</span><span class="p">)</span>
<span class="k">if</span> <span class="o">!</span><span class="n">hmac</span><span class="o">.</span><span class="n">Equal</span><span class="p">(</span><span class="n">passwordHash</span><span class="p">,</span> <span class="n">expectedHash</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">false</span>
<span class="p">}</span>
<span class="k">return</span> <span class="no">true</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">generateNonce</span><span class="p">(</span><span class="n">s</span> <span class="n">sessions</span><span class="o">.</span><span class="n">Session</span><span class="p">)</span> <span class="kt">uint64</span> <span class="p">{</span>
<span class="n">nonce</span> <span class="o">:=</span> <span class="n">rand</span><span class="o">.</span><span class="n">Uint64</span><span class="p">()</span>
<span class="n">s</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"nonce"</span><span class="p">,</span> <span class="n">nonce</span><span class="p">)</span>
<span class="n">s</span><span class="o">.</span><span class="n">Save</span><span class="p">()</span>
<span class="k">return</span> <span class="n">nonce</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">router</span> <span class="o">:=</span> <span class="n">gin</span><span class="o">.</span><span class="n">Default</span><span class="p">()</span>
<span class="n">store</span> <span class="o">:=</span> <span class="n">cookie</span><span class="o">.</span><span class="n">NewStore</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"secret"</span><span class="p">))</span>
<span class="n">router</span><span class="o">.</span><span class="n">Use</span><span class="p">(</span><span class="n">sessions</span><span class="o">.</span><span class="n">Sessions</span><span class="p">(</span><span class="s">"session"</span><span class="p">,</span> <span class="n">store</span><span class="p">))</span>
<span class="n">router</span><span class="o">.</span><span class="n">SetTrustedProxies</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">LoadHTMLFiles</span><span class="p">(</span><span class="s">"index.html"</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">GET</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="n">generateNonce</span><span class="p">(</span><span class="n">sessions</span><span class="o">.</span><span class="n">Default</span><span class="p">(</span><span class="n">c</span><span class="p">))</span>
<span class="n">c</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="s">"index.html"</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">GET</span><span class="p">(</span><span class="s">"/api/login/nonce"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span>
<span class="s">"nonce"</span><span class="o">:</span> <span class="n">strconv</span><span class="o">.</span><span class="n">FormatUint</span><span class="p">(</span><span class="n">sessions</span><span class="o">.</span><span class="n">Default</span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"nonce"</span><span class="p">)</span><span class="o">.</span><span class="p">(</span><span class="kt">uint64</span><span class="p">),</span> <span class="m">10</span><span class="p">),</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">POST</span><span class="p">(</span><span class="s">"/api/login"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="n">session</span> <span class="o">:=</span> <span class="n">sessions</span><span class="o">.</span><span class="n">Default</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
<span class="n">nonce</span> <span class="o">:=</span> <span class="n">session</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"nonce"</span><span class="p">)</span><span class="o">.</span><span class="p">(</span><span class="kt">uint64</span><span class="p">)</span>
<span class="n">generateNonce</span><span class="p">(</span><span class="n">session</span><span class="p">)</span>
<span class="k">var</span> <span class="n">reqBody</span> <span class="n">Login</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">BindJSON</span><span class="p">(</span><span class="o">&</span><span class="n">reqBody</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusInternalServerError</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">()})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">isValidUser</span><span class="p">(</span><span class="n">reqBody</span><span class="o">.</span><span class="n">Email</span><span class="p">,</span> <span class="n">reqBody</span><span class="o">.</span><span class="n">Password</span><span class="p">,</span> <span class="n">nonce</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span>
<span class="s">"status"</span><span class="o">:</span> <span class="s">"ok"</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusUnauthorized</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="s">"unauthorized"</span><span class="p">})</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">Run</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya menggunakan <code class="language-plaintext highlighter-rouge">rand.Uint64()</code> untuk menghasilkan sebuah angka 64-bit yang acak sebagai <em>nonce</em>. Angka ini
kemudian disimpan ke dalam <em>session</em>. Saya juga menambahkan <em>endpoint</em> <code class="language-plaintext highlighter-rouge">/api/login/nonce</code> untuk mendapatkan nilai <em>nonce</em> yang tersimpan
pada <em>session</em> yang sedang aktif dalam bentuk <em>string</em>. Saya tidak menggunakan angka karena batas maksimum nilai angka literal
yang dapat diproses oleh JavaScript hanya 53-bit sementara angka <em>nonce</em> adalah 64-bit.</p>
<p>Selain itu, kode program di atas juga tidak memakai <code class="language-plaintext highlighter-rouge">STATIC_KEY</code> lagi. Nilai <em>hash</em> kini dihitung dengan <em>password</em> sebagai <em>key</em> dan <em>nonce</em>
sebagai nilai pada kalkulasi HMAC. Agar perbandingan <em>hash</em>-nya sama, saya juga perlu mengubah kode program JavaScript di halaman HTML
menjadi seperti berikut ini:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">form</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">output</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">output</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">submit</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">email</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">email</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">password</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">password</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">}</span><span class="s2">/api/login/nonce`</span><span class="p">,</span> <span class="p">{</span><span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span><span class="p">});</span>
<span class="kd">const</span> <span class="nx">nonce</span> <span class="o">=</span> <span class="p">(</span><span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">()).</span><span class="nx">nonce</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">encoder</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">key</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">importKey</span><span class="p">(</span><span class="dl">'</span><span class="s1">raw</span><span class="dl">'</span><span class="p">,</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">password</span><span class="p">),</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="na">hash</span><span class="p">:</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">SHA-1</span><span class="dl">'</span><span class="p">}},</span> <span class="kc">false</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">sign</span><span class="dl">'</span><span class="p">]);</span>
<span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">DataView</span><span class="p">(</span><span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span>
<span class="nx">value</span><span class="p">.</span><span class="nx">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">nonce</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">hash</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">sign</span><span class="p">(</span><span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Mengirim email [</span><span class="p">${</span><span class="nx">email</span><span class="p">}</span><span class="s2">] dan password [</span><span class="p">${</span><span class="nx">hash</span><span class="p">}</span><span class="s2">] ke backend`</span><span class="p">);</span>
<span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">}</span><span class="s2">/api/login`</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span><span class="nx">email</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">hash</span><span class="p">)}),</span>
<span class="p">});</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">+=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span> <span class="o">+</span> <span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Pada kode program JavaScript di atas, saya terlebih dahulu memanggil <em>endpoint</em> <code class="language-plaintext highlighter-rouge">/api/login/nonce</code> untuk mendapatkan nilai <em>nonce</em>
yang aktif. Setelah itu, saya menggunakan <code class="language-plaintext highlighter-rouge">DataView</code> untuk menerjemahkan <em>nonce</em> dalam bentuk <em>string</em> menjadi angka 64-bit big endian.
Sama seperti di Go, saya juga menggunakan <em>password</em> sebagai <em>key</em> dan <em>nonce</em> sebagai nilai pada kalkulasi HMAC.</p>
<p>Sekarang, bila saya menggunakan halaman web ini, setiap kali <code class="language-plaintext highlighter-rouge">/api/login</code> dipanggil, nilai <em>hash</em> yang dikirim selalu berbeda setiap
kali di-eksekusi walaupun <em>hash</em> tersebut untuk <em>password</em> yang sama, seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00099.png" alt="Hashing Dengan Nonce Angka" class="img-fluid rounded" /></p>
<p>Dengan teknik ini, proses <em>hashing</em> lebih aman dari serangan <em>replay attack</em>. Walaupun penyerang berhasil mendapatkan nilai <em>hash</em> yang
pernah terekam, nilai tersebut tidak bisa dipakai lagi untuk <em>login</em>.</p>
<h4 id="menambahkan-nonce-waktu-pada-proses-hashing">Menambahkan Nonce Waktu Pada Proses Hashing</h4>
<p>Salah satu kelemahan pada proses <em>hashing</em> di bagian sebelumnya adalah proses tersebut perlu menyimpan nilai <em>nonce</em> di sebuah tempat. Sebagai contoh,
saya menyimpan <em>nonce</em> di <em>session</em> dan menyediakan <em>endpoint</em> untuk mendapatkan nilai <em>nonce</em> yang sedang aktif. Ini membuat
proses <em>login</em> menjadi sedikit lebih kompleks dari biasanya. Sebagai alternatif, saya dapat menggunakan waktu sebagai nilai
<em>nonce</em> karena waktu selalu unik dan tidak dapat diputar ulang.</p>
<p>Sebagai contoh, saya akan mengubah kode program Go yang saya buat menjadi seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"crypto/hmac"</span>
<span class="s">"crypto/sha1"</span>
<span class="s">"encoding/binary"</span>
<span class="s">"github.com/gin-gonic/gin"</span>
<span class="s">"net/http"</span>
<span class="s">"time"</span>
<span class="p">)</span>
<span class="k">const</span> <span class="n">EMAIL</span> <span class="o">=</span> <span class="s">"admin@jocki.me"</span>
<span class="k">const</span> <span class="n">PASSWORD</span> <span class="o">=</span> <span class="s">"password_rahasia_saya"</span>
<span class="k">type</span> <span class="n">Login</span> <span class="k">struct</span> <span class="p">{</span>
<span class="n">Email</span> <span class="kt">string</span> <span class="s">`json:"email" binding:"required"`</span>
<span class="n">Password</span> <span class="p">[]</span><span class="kt">byte</span> <span class="s">`json:"password" binding:"required"`</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">hash</span><span class="p">(</span><span class="n">password</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">nonce</span> <span class="kt">uint64</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
<span class="n">hmacSha1</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha1</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="n">value</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">8</span><span class="p">)</span>
<span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">PutUint64</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">nonce</span><span class="p">)</span>
<span class="n">hmacSha1</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">return</span> <span class="n">hmacSha1</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">isValidUser</span><span class="p">(</span><span class="n">email</span> <span class="kt">string</span><span class="p">,</span> <span class="n">passwordHash</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">nonce</span> <span class="kt">uint64</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">email</span> <span class="o">!=</span> <span class="n">EMAIL</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">false</span>
<span class="p">}</span>
<span class="n">expectedHash</span> <span class="o">:=</span> <span class="n">hash</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">PASSWORD</span><span class="p">),</span> <span class="n">nonce</span><span class="p">)</span>
<span class="k">if</span> <span class="o">!</span><span class="n">hmac</span><span class="o">.</span><span class="n">Equal</span><span class="p">(</span><span class="n">passwordHash</span><span class="p">,</span> <span class="n">expectedHash</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="no">false</span>
<span class="p">}</span>
<span class="k">return</span> <span class="no">true</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">generateNonce</span><span class="p">()</span> <span class="kt">uint64</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">uint64</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Unix</span><span class="p">())</span> <span class="o">/</span> <span class="kt">uint64</span><span class="p">(</span><span class="m">30</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">router</span> <span class="o">:=</span> <span class="n">gin</span><span class="o">.</span><span class="n">Default</span><span class="p">()</span>
<span class="n">router</span><span class="o">.</span><span class="n">SetTrustedProxies</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">LoadHTMLFiles</span><span class="p">(</span><span class="s">"index.html"</span><span class="p">)</span>
<span class="n">router</span><span class="o">.</span><span class="n">GET</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="s">"index.html"</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">POST</span><span class="p">(</span><span class="s">"/api/login"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
<span class="k">var</span> <span class="n">reqBody</span> <span class="n">Login</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">BindJSON</span><span class="p">(</span><span class="o">&</span><span class="n">reqBody</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusInternalServerError</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">()})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">isValidUser</span><span class="p">(</span><span class="n">reqBody</span><span class="o">.</span><span class="n">Email</span><span class="p">,</span> <span class="n">reqBody</span><span class="o">.</span><span class="n">Password</span><span class="p">,</span> <span class="n">generateNonce</span><span class="p">())</span> <span class="p">{</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span>
<span class="s">"status"</span><span class="o">:</span> <span class="s">"ok"</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="n">c</span><span class="o">.</span><span class="n">JSON</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusUnauthorized</span><span class="p">,</span> <span class="n">gin</span><span class="o">.</span><span class="n">H</span><span class="p">{</span><span class="s">"error"</span><span class="o">:</span> <span class="s">"unauthorized"</span><span class="p">})</span>
<span class="p">})</span>
<span class="n">router</span><span class="o">.</span><span class="n">Run</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya menghasilkan <em>nonce</em> berdasarkan ekspresi <code class="language-plaintext highlighter-rouge">time.Now().Unix() / 30</code> sehingga selama 30 detik akan
selalu menghasilkan <em>nonce</em> yang sama. Hal ini saya lakukan untuk mendukung selisih waktu maksimal 30 detik antara jam di <em>browser</em>
dengan jam di <em>server</em>.</p>
<p>Berikutnya, saya akan mengubah kode program JavaScript di HTML agar menggunakan proses yang sama dalam menghasilkan <em>hash</em> seperti
yang terlihat pada contoh berikut ini:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">form</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">output</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">output</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">submit</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">email</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">email</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">password</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">password</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">nonce</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">((</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">/</span> <span class="mi">30</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">encoder</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">key</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">importKey</span><span class="p">(</span><span class="dl">'</span><span class="s1">raw</span><span class="dl">'</span><span class="p">,</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">password</span><span class="p">),</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="na">hash</span><span class="p">:</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">SHA-1</span><span class="dl">'</span><span class="p">}},</span> <span class="kc">false</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">sign</span><span class="dl">'</span><span class="p">]);</span>
<span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">DataView</span><span class="p">(</span><span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span>
<span class="nx">value</span><span class="p">.</span><span class="nx">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">BigInt</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">nonce</span><span class="p">}</span><span class="s2">`</span><span class="p">));</span>
<span class="kd">let</span> <span class="nx">hash</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">sign</span><span class="p">(</span><span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Mengirim email [</span><span class="p">${</span><span class="nx">email</span><span class="p">}</span><span class="s2">] dan password [</span><span class="p">${</span><span class="nx">hash</span><span class="p">}</span><span class="s2">] ke backend`</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">}</span><span class="s2">/api/login`</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span><span class="nx">email</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">hash</span><span class="p">)}),</span>
<span class="p">});</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">+=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span> <span class="o">+</span> <span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya juga menggunakan <em>step</em> <code class="language-plaintext highlighter-rouge">30</code> detik dalam menghitung nilai <em>nonce</em>. Nilai ini harus sama dengan nilai
yang saya pakai di <em>server</em>.</p>
<p>Sekarang, bila saya <em>login</em> berkali-kali di web, selama 30 detik, halaman <em>web</em> akan selalu mengirim <em>hash</em> yang sama. Setelah 30 detik
berlalu, bila saya mencoba <em>login</em> lagi, <em>hash</em> yang dikirim ke <em>server</em> akan berbeda, seperti yang terlihat pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00100.png" alt="Hashing Dengan Nonce Waktu" class="img-fluid rounded" /></p>
<p>Walaupun teknik ini masih memungkinkan <em>replay attack</em> selama 30 detik, namun teknik ini membuat proses <em>login</em> menjadi lebih
sederhana karena tidak melibatkan pertukaran angka <em>nonce</em>. Teknik ini merupakan alternatif yang lebih disarankan
terutama bila jam di <em>server</em> dan jam di <em>browser</em> dapat dipastikan ter-sinkronisasi dengan baik.</p>Jocki HendryProses hashing untuk password di sisi frontend biasanya dilakukan supaya password tidak dikirimkan apa adanya (plain text) melalui jaringan. Secara umum, proses ini tidak begitu meningkatkan keamanan password karena website modern sudah menggunakan HTTPS sehingga password yang dikirim ke backend sudah ter-enkripsi. Proses hashing ini lebih berguna untuk serangan tertentu seperti MITM proxy dan mencegah password tidak sengaja tersimpan di log backend (misalnya di server NGINX yang men-log seluruh request body).Memakai Algoritma HOTP & TOTP2023-02-16T00:00:00+00:002023-02-16T00:00:00+00:00https://blog.jocki.me/pemograman/2023/02/16/memakai-algoritma-HOTP-TOTP<p>Salah satu algoritma yang paling sering digunakan untuk menghasilkan <em>one-time password</em> (OTP) adalah algoritma <em>HMAC-based
one-time password</em> (HOTP) dan <em>Time-based one-time password</em> (TOTP). Sebagai contoh, Google Authenticator mendukung kedua
algoritma tersebut dimana HOTP disebut sebagai <em>counter based</em> dan TOTP disebut sebagai <em>time based</em> seperti yang diperlihatkan pada
gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00096.png" alt="Tampilan Google Authenticator" class="img-fluid rounded" /></p>
<h4 id="algoritma-hotp">Algoritma HOTP</h4>
<p>Algoritma HOTP lebih sering dipakai pada token perangkat keras. Algoritma ini bergantung pada nilai <em>counter</em> yang harus
sama antara perangkat yang menghasilkan OTP dan <em>backend</em> yang melakukan validasi (biasanya tanpa komunikasi langsung).</p>
<p><a href="https://www.ietf.org/rfc/rfc4226.txt">RFC 4226</a> mendefinisikan algoritma HOTP (HMAC-Based One-Time Password) sebagai:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
</code></pre></div></div>
<p>Agar lebih jelas, saya akan mencoba membuat kode program Go yang melakukan kalkulasi HOTP. Saya akan mulai dengan menulis
kode program seperti berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">k</span> <span class="o">:=</span> <span class="s">"OJQWQYLTNFQWU33DNNUSCIIK"</span>
<span class="n">c</span> <span class="o">:=</span> <span class="kt">uint64</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
<span class="n">b</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">base32</span><span class="o">.</span><span class="n">StdEncoding</span><span class="o">.</span><span class="n">DecodeString</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>
<span class="n">hmacSha1</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha1</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
<span class="n">cb</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">8</span><span class="p">)</span>
<span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">PutUint64</span><span class="p">(</span><span class="n">cb</span><span class="p">,</span> <span class="n">c</span><span class="p">)</span>
<span class="n">hmacSha1</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">cb</span><span class="p">)</span>
<span class="n">hash</span> <span class="o">:=</span> <span class="n">hmacSha1</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya menggunakan <em>secret</em> <code class="language-plaintext highlighter-rouge">k</code> dengan nilai <code class="language-plaintext highlighter-rouge">"OJQWQYLTNFQWU33DNNUSCIIK"</code>. Walaupun bukan bagian dari RFC 4226,
Google Authenticator menggunakan versi <em>base32 encoded</em> dari <em>secret</em> yang sesungguhnya. Sebagai contoh, karena nilai <em>secret</em>
saya adalah <code class="language-plaintext highlighter-rouge">rahasiajocki!!</code>, saya bisa mendapatkan nilai <em>base32 encoded</em> dengan memberikan perintah seperti berikut ini:</p>
<blockquote>
<p><strong>$</strong> <code>echo 'rahasiajocki!!' | base32</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OJQWQYLTNFQWU33DNNUSCIIK
</code></pre></div></div>
<p>Hasil dari perintah di atas juga adalah nilai yang <em>valid</em> untuk dipakai sebagai <em>secret key</em> di Google Authenticator.</p>
<p>Nilai <code class="language-plaintext highlighter-rouge">c</code> yang saya pakai berupa <code class="language-plaintext highlighter-rouge">1</code>. Sesuai spesifikasi RFC 4226, nilai ini bertipe 8 bytes (<code class="language-plaintext highlighter-rouge">uint64</code>). Ini juga
merupakan nilai yang saya sertakan sebagai masukan untuk kalkulasi HMAC-SHA-1. Hasil dari kalkulasi tersebut akan disimpan
di variabel <code class="language-plaintext highlighter-rouge">hash</code>. Nilai <code class="language-plaintext highlighter-rouge">hash</code> selalu memiliki ukuran 160 bits (20 bytes).</p>
<p>Bagian berikutnya adalah melakukan konversi <code class="language-plaintext highlighter-rouge">hash</code> menjadi angka singkat yang mudah diketik oleh pengguna. Sebagai contoh, untuk
menghasilkan 6 digit angka dari <code class="language-plaintext highlighter-rouge">hash</code>, saya melanjutkan kode program di atas dengan menambahkan bagian berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">offset</span> <span class="o">:=</span> <span class="n">hash</span><span class="p">[</span><span class="m">19</span><span class="p">]</span> <span class="o">&</span> <span class="m">0xf</span>
</code></pre></div></div>
<p>Pada kode program di atas, saya melakukan <em>masking</em> dengan <code class="language-plaintext highlighter-rouge">0xf</code> untuk mendapatkan nilai <code class="language-plaintext highlighter-rouge">offset</code> dari <em>byte</em> paling terakhir dari <code class="language-plaintext highlighter-rouge">hash</code>. Nilai ini
akan selalu berada dalam batasan <code class="language-plaintext highlighter-rouge">0</code> hingga <code class="language-plaintext highlighter-rouge">15</code>. Setelah mendapatkan nilai <code class="language-plaintext highlighter-rouge">offset</code>, saya perlu mengambil nilai dari 4 <em>byte</em> <code class="language-plaintext highlighter-rouge">hash</code> mulai dari
posisi <code class="language-plaintext highlighter-rouge">offset</code> hingga <code class="language-plaintext highlighter-rouge">offset+3</code>. Sebagai contoh, bila nilai <code class="language-plaintext highlighter-rouge">offset</code> adalah <code class="language-plaintext highlighter-rouge">3</code>, saya akan mengambil <em>byte</em> di posisi <code class="language-plaintext highlighter-rouge">3</code>, <code class="language-plaintext highlighter-rouge">4</code>, <code class="language-plaintext highlighter-rouge">5</code>, <code class="language-plaintext highlighter-rouge">6</code>.
Bila nilai <code class="language-plaintext highlighter-rouge">offset</code> adalah <code class="language-plaintext highlighter-rouge">15</code>, saya akan mengambil <em>byte</em> di posisi <code class="language-plaintext highlighter-rouge">15</code>, <code class="language-plaintext highlighter-rouge">16</code>, <code class="language-plaintext highlighter-rouge">17</code>, <code class="language-plaintext highlighter-rouge">18</code>. Saya dapat melakukannya dengan menggunakan
kode program berikut ini:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">binCode</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">{</span><span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="p">]</span> <span class="o">&</span> <span class="m">0x7f</span><span class="p">,</span> <span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="o">+</span><span class="m">1</span><span class="p">],</span> <span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="o">+</span><span class="m">2</span><span class="p">],</span> <span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="o">+</span><span class="m">3</span><span class="p">]}</span>
</code></pre></div></div>
<p>Saya melakukan <em>masking</em> nilai <code class="language-plaintext highlighter-rouge">hash[offset]</code> dengan <code class="language-plaintext highlighter-rouge">0x7f</code> karena pada spesifikasi RFC 4226, hanya 31 bit terakhir yang diambil. Karena 4 bytes terdiri
atas 32 bit, saya perlu membuang bit pertama dari <code class="language-plaintext highlighter-rouge">hash[offset]</code>. Setelah ini, saya menambahkan kode program ini untuk menerjemahkan <code class="language-plaintext highlighter-rouge">binCode</code> menjadi
sebuah angka 32 bit dan menggunakan modulus untuk mendapatkan angka 6 digit:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dbc</span> <span class="o">:=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(</span><span class="n">binCode</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">dbc</span> <span class="o">%</span> <span class="kt">uint32</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">Pow</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">6</span><span class="p">)))</span>
</code></pre></div></div>
<p>Kode program di atas akan mengembalikan nilai <code class="language-plaintext highlighter-rouge">231384</code> sebagai token yang perlu dimasukkan oleh pengguna. Bila saya menggunakan Google Authenticator
untuk menambahkan <em>secret key</em> <code class="language-plaintext highlighter-rouge">OJQWQYLTNFQWU33DNNUSCIIK</code> dengan tipe <strong>Counter based</strong>, saat <em>counter</em> bernilai <code class="language-plaintext highlighter-rouge">1</code>, saya juga akan memperoleh nilai
<code class="language-plaintext highlighter-rouge">231384</code> yang sama.</p>
<p>Di HOTP, setiap kali saya meminta token baru, nilai <em>counter</em> akan ditingkatkan. Validator di sisi server dan generator HOTP perlu memiliki
mekanisme untuk melakukan sinkronisasi <em>counter</em> (tanpa berkomunikasi secara langsung). Bila terjadi perbedaan <em>counter</em> yang terlalu jauh,
token juga dapat dikunci untuk mencegah serangan <em>brute-force</em>.</p>
<h4 id="algoritma-totp">Algoritma TOTP</h4>
<p>Time-Based One-Time Password (TOTP) adalah pengembangan dari algoritma HOTP dimana penggunaan <em>counter</em> diganti menjadi waktu. <a href="https://www.ietf.org/rfc/rfc6238.txt">RFC 6238</a> mendeklarasikan
algoritma TOTP sebagai:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TOTP = HOTP(K, T)
</code></pre></div></div>
<p>Nilai <code class="language-plaintext highlighter-rouge">T</code> sendiri didefinisikan sebagai:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>T = (Current Unix Time - T0) / X
</code></pre></div></div>
<p>dimana nilai <code class="language-plaintext highlighter-rouge">T0</code> adalah nilai awal dari Unix time yang masuk dalam perhitungan (<em>default</em>-nya adalah <code class="language-plaintext highlighter-rouge">0</code>) dan nilai <code class="language-plaintext highlighter-rouge">X</code> adalah
nilai <em>step</em> dalam detik (<em>default</em>-nya adalah 30 detik).</p>
<p>Dengan demikian, saya bisa menulis algoritma TOTP sebagai:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
<span class="s">"crypto/hmac"</span>
<span class="s">"encoding/base32"</span>
<span class="s">"encoding/binary"</span>
<span class="s">"math"</span>
<span class="s">"time"</span>
<span class="p">)</span>
<span class="k">import</span> <span class="s">"crypto/sha1"</span>
<span class="k">func</span> <span class="n">hotp</span><span class="p">(</span><span class="n">k</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">c</span> <span class="kt">uint64</span><span class="p">)</span> <span class="kt">uint32</span> <span class="p">{</span>
<span class="n">hmacSha1</span> <span class="o">:=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">sha1</span><span class="o">.</span><span class="n">New</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
<span class="n">cb</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">8</span><span class="p">)</span>
<span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">PutUint64</span><span class="p">(</span><span class="n">cb</span><span class="p">,</span> <span class="n">c</span><span class="p">)</span>
<span class="n">hmacSha1</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">cb</span><span class="p">)</span>
<span class="n">hash</span> <span class="o">:=</span> <span class="n">hmacSha1</span><span class="o">.</span><span class="n">Sum</span><span class="p">(</span><span class="no">nil</span><span class="p">)</span>
<span class="n">offset</span> <span class="o">:=</span> <span class="n">hash</span><span class="p">[</span><span class="m">19</span><span class="p">]</span> <span class="o">&</span> <span class="m">0xf</span>
<span class="n">binCode</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">{</span><span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="p">]</span> <span class="o">&</span> <span class="m">0x7f</span><span class="p">,</span> <span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="o">+</span><span class="m">1</span><span class="p">],</span> <span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="o">+</span><span class="m">2</span><span class="p">],</span> <span class="n">hash</span><span class="p">[</span><span class="n">offset</span><span class="o">+</span><span class="m">3</span><span class="p">]}</span>
<span class="n">dbc</span> <span class="o">:=</span> <span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint32</span><span class="p">(</span><span class="n">binCode</span><span class="p">)</span>
<span class="k">return</span> <span class="n">dbc</span> <span class="o">%</span> <span class="kt">uint32</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">Pow</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">6</span><span class="p">))</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">totp</span><span class="p">(</span><span class="n">k</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="kt">uint32</span> <span class="p">{</span>
<span class="n">now</span> <span class="o">:=</span> <span class="kt">uint64</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Unix</span><span class="p">())</span>
<span class="n">t0</span> <span class="o">:=</span> <span class="kt">uint64</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
<span class="n">x</span> <span class="o">:=</span> <span class="kt">uint64</span><span class="p">(</span><span class="m">30</span><span class="p">)</span>
<span class="n">t</span> <span class="o">:=</span> <span class="p">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">t0</span><span class="p">)</span> <span class="o">/</span> <span class="n">x</span>
<span class="k">return</span> <span class="n">hotp</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">t</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">k</span> <span class="o">:=</span> <span class="s">"OJQWQYLTNFQWU33DNNUSCIIK"</span>
<span class="n">kb</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">base32</span><span class="o">.</span><span class="n">StdEncoding</span><span class="o">.</span><span class="n">DecodeString</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">totp</span><span class="p">(</span><span class="n">kb</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Bila saya membandingkan hasil dari kode program di atas dengan nilai dari Google Authenticator, saya akan memperoleh
hasil yang sama, seperti yang diperlihatkan pada gambar berikut ini:</p>
<p><img src="/assets/images/gambar_00097.png" alt="Perbandingan Hasil Kode Program dan Google Authenticator" class="img-fluid rounded" />
keys</p>Jocki HendrySalah satu algoritma yang paling sering digunakan untuk menghasilkan one-time password (OTP) adalah algoritma HMAC-based one-time password (HOTP) dan Time-based one-time password (TOTP). Sebagai contoh, Google Authenticator mendukung kedua algoritma tersebut dimana HOTP disebut sebagai counter based dan TOTP disebut sebagai time based seperti yang diperlihatkan pada gambar berikut ini: