Commit 0bd9a8c
Changed files (4)
002.TOTP
002.TOTP/index.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>002.TOTP</title>
+ <link rel="stylesheet" href="./style.css">
+ <link rel="stylesheet" href="../assets/common.css">
+ <link rel="shortcut icon" href="../assets/favicon.png" type="image/png">
+</head>
+
+<body>
+ <!-- Modal Section -->
+ <input class="modal-state" id="modal" type="checkbox" />
+ <div class="modal">
+ <label class="modal__bg" for="modal"></label>
+ <div class="modal__inner" style="width:25%;">
+ <label class="modal__close" for="modal"></label>
+ <h2>002.TOTP</h2>
+ <p class="modal__p">
+ <ul>
+ <li style="margin-bottom:10px">
+ TOTP works by taking a <span style="color:rgb(212, 175, 55)">secret</span> and a <span
+ style="color:rgb(212, 175, 55)">timestamp</span> and hashing them together.
+ </li>
+ <li style="margin-bottom:10px">
+ The <span style="color:rgb(212, 175, 55)">hash</span> is then truncated to a certain number of
+ digits.
+ </li>
+ <li style="margin-bottom:10px">
+ The digits are then used to generate a <span style="color:rgb(212, 175, 55)">one-time
+ password</span>.
+ </li>
+ <li style="margin-bottom:10px">
+ The <span style="color:rgb(212, 175, 55)">period</span> is the time in seconds that the <span
+ style="color:rgb(212, 175, 55)">one-time password</span> is valid.
+ </li>
+ <li>
+ Refer: <a href="https://tools.ietf.org/html/rfc6238" target="_blank"
+ style="color:rgb(212, 175, 55)">RFC
+ 6238</a>.
+ </li>
+ </ul>
+
+ </p>
+ </div>
+ </div>
+
+ <!-- Main Content -->
+ <div class="container">
+ <div class="group">
+ <label for="secret">Secret:</label>
+ <input type="text" id="secret" name="secret" value="JV4SA43BMZSQ">
+ </div>
+ <div class="group">
+ <label for="period">Period:</label>
+ <input type="number" id="period" name="period" value="30" min="3">
+ </div>
+ <div class="group">
+ <label for="digits">Digits:</label>
+ <input type="number" id="digits" name="digits" value="6" min="1" max="10">
+ </div>
+ <p id="remaining"></p>
+ <p id="otp"></p>
+ <svg class="dial">
+ <circle r="230" cx="120" cy="140" fill="transparent" stroke="rgb(212, 175, 55,0.7)" stroke-width="10"
+ transform="rotate(-90 100 100)" stroke-dasharray="1570" stroke-dashoffset="0"></circle>
+ </svg>
+ </div>
+
+ <!-- Footer Section -->
+ <div class="footer">
+ <label for="modal">(i)</label>
+ <a class="arrow" href="../"></a>
+ </div>
+</body>
+<script src="./script.js"></script>
+
+</html>
\ No newline at end of file
002.TOTP/script.js
@@ -0,0 +1,120 @@
+const secret = document.getElementById('secret');
+const period = document.getElementById('period');
+const digits = document.getElementById('digits');
+const circle = document.querySelector("circle");
+const perimeter = circle.getAttribute("r") * 2 * Math.PI;
+
+function base32ToBytes(base32) {
+ const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+ let bits = '';
+ for (const char of base32) {
+ const val = base32Chars.indexOf(char.toUpperCase());
+ if (val === -1) {
+ throw new Error("Invalid Base32 character");
+ }
+ bits += val.toString(2).padStart(5, '0');
+ }
+ const bytes = [];
+ for (let i = 0; i < bits.length; i += 8) {
+ const byte = bits.substring(i, i + 8);
+ if (byte.length === 8) {
+ bytes.push(parseInt(byte, 2));
+ }
+ }
+ return new Uint8Array(bytes);
+}
+
+// Helper function to convert integer to byte array
+function intToBytes(num) {
+ const bytes = new Uint8Array(8);
+ for (let i = 7; i >= 0; i--) {
+ bytes[i] = num & 0xff;
+ num = num >> 8;
+ }
+ return bytes;
+}
+
+// HMAC-SHA1 function using Web Crypto API
+async function hmacSha1(key, message) {
+ const cryptoKey = await crypto.subtle.importKey(
+ 'raw',
+ key,
+ { name: 'HMAC', hash: 'SHA-1' },
+ false,
+ ['sign']
+ );
+ const signature = await crypto.subtle.sign('HMAC', cryptoKey, message);
+ return new Uint8Array(signature);
+}
+
+async function generateTOTP({
+ seed,
+ timePeriod,
+ length
+}) {
+ const key = base32ToBytes(seed);
+ const epoch = Math.floor(Date.now() / 1000);
+ const time = Math.floor(epoch / timePeriod);
+ const timeBytes = intToBytes(time);
+ const hmac = await hmacSha1(key, timeBytes);
+
+ const offset = hmac[hmac.length - 1] & 0x0f;
+ const binary = (
+ ((hmac[offset] & 0x7f) << 24) |
+ ((hmac[offset + 1] & 0xff) << 16) |
+ ((hmac[offset + 2] & 0xff) << 8) |
+ (hmac[offset + 3] & 0xff)
+ );
+
+ const otp = binary % 10 ** length;
+ return otp.toString().padStart(length, '0');
+}
+
+async function setTOTP() {
+ const timeRemaining = (period.value - (Date.now() / 1000) % period.value).toFixed(0);
+ if (timeRemaining <= 0) {
+ return;
+ }
+ const otp = await generateTOTP({
+ seed: secret.value,
+ timePeriod: period.value,
+ length: digits.value
+ });
+ document.getElementById('otp').innerText = otp;
+ document.getElementBy
+}
+
+secret.addEventListener('input', async (e) => {
+ e.target.value = e.target.value.toUpperCase();
+ await setTOTP();
+});
+
+period.addEventListener('input', async (e) => {
+ if (e.target.value < 3) {
+ e.target.value = 3;
+ }
+ await setTOTP();
+});
+
+digits.addEventListener('input', async (e) => {
+ if (e.target.value < 1) {
+ e.target.value = 1;
+ }
+ if (e.target.value > 10) {
+ e.target.value = 10;
+ }
+ await setTOTP();
+});
+
+function updateCircle() {
+ const timeRemaining = (period.value - (Date.now() / 1000) % period.value).toFixed(0);
+ circle.setAttribute(
+ "stroke-dashoffset",
+ (perimeter * timeRemaining) / period.value - perimeter
+ );
+ document.getElementById("remaining").innerText = `Remaining: ${timeRemaining}s`;
+}
+
+updateCircle()
+setTOTP()
+setInterval(() => { updateCircle(); setTOTP() }, 1000);
\ No newline at end of file
002.TOTP/style.css
@@ -0,0 +1,39 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ height: fit-content;
+ z-index: 10;
+}
+
+.group{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 300px;
+ font-size: 20px;
+}
+
+#otp{
+ font-weight: 500;
+ font-style: normal;
+ font-size: 46px;
+ letter-spacing: 0.3em;
+ width: 100%;
+ text-align: center;
+ margin-top: 20px;
+}
+
+#remaining{
+ margin-top: 20px;
+}
+
+.dial{
+ position:absolute;
+ overflow: visible;
+ z-index: 1;
+ pointer-events: none;
+}
index.html
@@ -14,6 +14,7 @@
</h1>
<div class="flex flex-col text-white gap-x-[200px] gap-y-2 p-10">
<a href="./001.Vision/" class="opacity-75 hover:opacity-100 hover:text-yellow-400">001.Vision</a>
+ <a href="./002.TOTP/" class="opacity-75 hover:opacity-100 hover:text-yellow-400" >002.TOTP</a>
</div>
</body>
</html>
\ No newline at end of file