ワンタイムパスワードの6桁数値を取得
URLから開くだけで勝手にコピペしてつかえる
Authenticator認証アプリに表示されるQRコードから
otpauth://totp/zzzz?secret=XXXX&issuer=YYYY の赤字部分XXXXを入力
ワンタイムパスワードの6桁数値を生成
https:qiita.com/kerupani129/items/4780fb1eea160c7a00bd
view source
JavaScript
document.title = 'ワンタイムパスワードの6桁数値を生成';
//https://qiita.com/kerupani129/items/4780fb1eea160c7a00bd
// メモ: RFC 4648 で定義されている Base32 文字
const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const encodeBase32 = uint8Array => {
const byteLength = uint8Array.byteLength;
let dataBuffer = 0;
let dataBufferBitLength = 0;
let byteOffset = 0;
let result = '';
// バッファにデータが残っているか、またはバッファに読み込めるデータが残っていたら継続
while ( dataBufferBitLength > 0 || byteOffset < byteLength ) {
// バッファのデータが少なければデータを追加する
if ( dataBufferBitLength < 5 ) {
if ( byteOffset < byteLength ) {
// 読み込めるデータが残っていたら読み込む
dataBuffer <<= 8;
dataBuffer |= uint8Array[byteOffset++];
dataBufferBitLength += 8;
} else {
// 読み込めるデータがなければ値が 0 のパディングビットを追加して長さを 5 ビットにする
dataBuffer <<= 5 - dataBufferBitLength;
dataBufferBitLength = 5;
}
}
// バッファのデータの左の長さ 5 ビットの値を取得する
dataBufferBitLength -= 5;
const value = dataBuffer >>> dataBufferBitLength & 0x1f;
// 値を Base32 文字に変換
result += base32Alphabet[value];
}
// パディング文字 '=' を追加
const targetLength = Math.ceil(result.length / 8) * 8;
const resultPadded = result.padEnd(targetLength, '=');
return resultPadded;
};
// メモ: RFC 4648 で定義されている Base32 文字
//const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const base32AlphabetValuesMap = new Map([
...Array.from(base32Alphabet, (encoding, value) => [encoding, value]),
...Array.from(base32Alphabet.toLowerCase(), (encoding, value) => [encoding, value]),
]);
/*
* WARNING: Base32 デコード時に大文字と小文字を区別しないことによる脆弱性や不具合に注意
* 参考: 12. Security Considerations - RFC 4648 - The Base16, Base32, and Base64 Data Encodings
* https://www.ietf.org/rfc/rfc4648.html#section-12
*/
const decodeBase32 = string => {
// メモ: 正しく Base32 エンコードされてパディングされていれば長さが 8 の倍数のはず
// 長さが 8 の倍数でない場合はパディングされていないかまたは正しく Base32 エンコードされていない
if ( (string.length & 0x7) !== 0 ) throw new Error('Invalid base32 string');
// 末尾のパディング文字 '=' を除去する
const stringTrimmed = string.replace(/=*$/, '');
// メモ: デコード後のサイズは切り捨てで計算する
const result = new Uint8Array(stringTrimmed.length * 5 >>> 3);
let dataBuffer = 0;
let dataBufferBitLength = 0;
let byteOffset = 0;
for (const encoding of stringTrimmed) {
const value = base32AlphabetValuesMap.get(encoding);
if ( typeof value === 'undefined' ) throw new Error('Invalid base32 string');
// バッファに長さ 5 ビットの値を読み込む
dataBuffer <<= 5;
dataBuffer |= value;
dataBufferBitLength += 5;
// バッファのデータが少なければデータを取得する
if ( dataBufferBitLength >= 8 ) {
dataBufferBitLength -= 8;
result[byteOffset++] = dataBuffer >>> dataBufferBitLength;
}
}
// 正しく Base32 エンコードされたデータであれば残る長さは 5 ビット未満のはず
// 5 ビット以上残った場合は正しく Base32 エンコードされていない
if ( dataBufferBitLength >= 5 ) throw new Error('Invalid base32 string');
// 正しく Base32 エンコードされたデータであれば残る値は 0 のはず
// 0 以外のデータが残った場合は正しく Base32 エンコードされていない
if ( (dataBuffer << (4 - dataBufferBitLength) & 0xf) !== 0 ) throw new Error('Invalid base32 string');
return result;
};
//
const base32 = 'CAQDAQCQMA======';
console.log(base32);
const uint8ArrayB = decodeBase32(base32);
console.log(uint8ArrayB.toString());
// 鍵を生成
const seedUint8Array = new Uint8Array(20);
crypto.getRandomValues(seedUint8Array);
console.log(seedUint8Array.toString())
console.log(Array.from(seedUint8Array, uint8 => `0x${uint8.toString(16).padStart(2, '0')}`).join(', '));
const seedString = encodeBase32(seedUint8Array);
console.log(seedString);
//
//
const hmacSHA1 = async (keyUint8Array, dataUint8Array) => {
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyUint8Array,
{
name: 'HMAC',
hash: { name: 'SHA-1' },
},
false,
['sign'],
);
const signatureArrayBuffer = await crypto.subtle.sign('HMAC', cryptoKey, dataUint8Array);
const signatureUint8Array = new Uint8Array(signatureArrayBuffer);
return signatureUint8Array;
};
/**
* 動的切り捨てする
*/
const dynamicTruncate = digestUint8Array => {
// 下位 4 ビットを offset とする
// digestUint8Array.length === 20;
const offset = digestUint8Array[19] & 0xf;
// offset から 4 バイトの数値を得る
const binary = (
digestUint8Array[offset ] << 24 |
digestUint8Array[offset + 1] << 16 |
digestUint8Array[offset + 2] << 8 |
digestUint8Array[offset + 3]
);
// 符号有無の混乱を防ぐために最上位ビットを除外する
const binaryMasked = binary & 0x7fffffff;
return binaryMasked;
};
/**
* HOTP を計算する
*
* カウンタはビッグエンディアンで 8 バイト
*/
const generateHOTP = async (seedUint8Array, counterUint8Array) => {
const digestUint8Array = await hmacSHA1(seedUint8Array, counterUint8Array);
const otp = dynamicTruncate(digestUint8Array) % 1000000;
const otpString = otp.toString().padStart(6, '0');
return otpString;
};
// HOTP を計算する
/*
const seedUint8Array = new Uint8Array([0xdd, 0x1e, 0x26, 0x25, 0xa8, 0xda, 0x7f, 0xa3, 0xb8, 0x6b, 0x80, 0x9e, 0xd9, 0x20, 0xc8, 0x68, 0x8f, 0x53, 0x83, 0x46]);
const counterUint8Array = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
const hotp = await generateHOTP(seedUint8Array, counterUint8Array);
console.log(hotp);
*/
const timeStep = 30;//30
//
const getCurrentUnixTime = () => Math.floor(Date.now() / 1000);
const getCurrentSteps = () => Math.floor(getCurrentUnixTime() / timeStep);
/**
* 整数をビッグエンディアン形式で Uint8Array に変換する
*/
const intToUint8ArrayInBigEndian = number => {
// メモ: number が負の場合、Math.floor() なしだと正しい結果を得られないため注意
const highOrderDigits = Math.floor(number / 0x1_00000000);
//
const uint8Array = new Uint8Array(8);
uint8Array[0] = highOrderDigits >>> 24 & 0xff;
uint8Array[1] = highOrderDigits >>> 16 & 0xff;
uint8Array[2] = highOrderDigits >>> 8 & 0xff;
uint8Array[3] = highOrderDigits & 0xff;
uint8Array[4] = number >>> 24 & 0xff;
uint8Array[5] = number >>> 16 & 0xff;
uint8Array[6] = number >>> 8 & 0xff;
uint8Array[7] = number & 0xff;
return uint8Array;
};
/**
* TOTP を計算する
*/
const generateTOTP = async (seedUint8Array, steps) => {
const key = new TextDecoder("utf-8").decode(seedUint8Array);
console.log(key)
const stepsUint8Array = intToUint8ArrayInBigEndian(steps);
const otpString = await generateHOTP(seedUint8Array, stepsUint8Array);
console.log(otpString)
document.getElementById('totp').textContent = otpString
return otpString;
};
//まだ正しい値が取得できない
//const secret = 'NIZDS5THLFDUEK2QNI2FK3CKKNGXUTL9';
const url = new URL(location.href);
const key = url.searchParams.get('key') || 'MMMMMMMMMMMMMMMM';
const encoded = new TextEncoder().encode(key);
const steps = getCurrentSteps();
document.getElementById('key').value = key
const base32key = key;
console.log(base32key);
const uint8ArrayKey = decodeBase32(base32key);
console.log(uint8ArrayB.toString());
generateTOTP(uint8ArrayKey,steps)
setInterval(function(){
let steps = getCurrentSteps();
generateTOTP(uint8ArrayKey,steps)
resetBar()
}, 30*1000)
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
console.log("クリップボードにコピーしました: " + text);
})
.catch(err => {
console.error("コピーに失敗しました:", err);
});
}
function resetBar() {
let bar = $('.bar').clone();
$('.bar-container').empty();
$('.bar-container').append(bar);
}
$(function(){
$(document).on('click', '#totp',function(){
copyToClipboard($(this).text());
});
setTimeout(function(){
$('#totp').trigger('click');
}, 499)
setTimeout(function(){
window.close()
}, 8*60*1000)
setTimeout(function(){
location.href = '/demo/-js/js-date-clock/';
}, 9*30*1000)
});
CSS
input{
width:320px;
}
b{
color:#ff0000;
}
#totp{
font-size:20vw;
width:99%;
}
.bar-container {
width: 100%;
height: 20px;
background-color: #ddd;
overflow: hidden;
}
.bar {
width: 100%;
height: 100%;
background-color: #333;
animation: shrink 30s linear forwards;
}
@keyframes shrink {
from {
width: 100%;
}
to {
width: 0%;
}
}
HTML
ページのソースを表示 : Ctrl+U , DevTools : F12
view-source:https://hi0a.com/demo/-js/js-totp/
ABOUT
hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。
プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。
必要な機能の関数をコピペ利用したり勉強に活用できます。
プログラムの動作サンプル結果は画面上部に出力表示されています。
環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。
画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。
仕様変更、API廃止等、実験途中で動かないページもあります。