ワンタイムQRの詳細仕様
ワンタイムQRとは
ワンタイムQRは、定期的に変化するQRコードによる認証システムです。従来の静的のQRコードとは異なり、複製による使いまわしのリスクを軽減することができます。
言葉の定義
ワンタイムQRに関連する言葉の定義を行います。
| 言葉 | 説明 |
|---|---|
フォーマットバージョン |
ワンタイムQRの文字列形式のバージョンです。将来的なフォーマット変更に備えて管理されており、現在はv=1が有効です。 |
TOTPシード |
TOTPアルゴリズム(RFC 6238準拠)でワンタイムパスワードを生成するための秘密鍵です。この値を基に現在時刻から6桁のワンタイムパスワードが計算されます。 |
タイムステップ |
ワンタイムQRが切り替わる時間間隔(秒数)です。デフォルトは300秒で、この間隔で異なるTOTPコードが生成されます。設定可能範囲は30秒〜86400秒(24時間)です。 |
許容ずれ |
時刻のずれや認証処理の遅れに対応するための設定です。現在の時間窓に加えて、過去・未来それぞれ何回分の時間窓のワンタイムQRを有効とするかを指定します。デフォルトは前後0回(合計1つの時間窓で認証可能)です。 |
タイムステップと許容ずれ
言葉の定義 にあるように、タイムステップとはTOTPコードが生成される時間間隔です。すなわち、ワンタイムQRが切り替わる時間を意味します。
「TOTPコードが切り替わる直前にQRコードデータを生成し、メンバーがワンタイムQRをかざしたときにはTOTPコードが切り替わってしまっていたので、認証に失敗してしまった。」という状況を防ぐために、許容ずれを設定することができます。
下図の例では、タイムステップを30秒、許容ずれを2回と設定した場合を表しています。
このとき、現在のステップにおけるTOTPコード「570740」は、前後2回分を含めた2分30秒間は有効となります。
フォーマット
ワンタイムQRのQRコードデータは以下の形式で構成されます。 SL-OTQR?v=1&data={認証データ}&totp={TOTPコード}
|
| 内容(*は必須) | 形式 | 説明 |
|---|---|---|
スキーマ識別子* |
|
ワンタイムQRのQRコードデータは |
フォーマットバージョン |
|
省略時も |
認証データ部* |
|
メンバーに登録した認証データを指定します。このデータを基に認証対象を特定します。ワンタイムQRを使用する場合、認証データに '&' は使用できません。 |
TOTPコード部* |
|
現在時刻から生成された6桁のワンタイムパスワードを指定します。タイムステップごとに変化します。詳細は _TOTPコードの計算方法 を参照してください。 |
|
ワンタイムQRは 半角557文字または全角235文字以下 の範囲内で生成してください。認証データや追加情報のサイズを大きくすると認証に失敗する可能性があります。 |
|
静的QRの設定がされているメンバーでは、設定したQRコードデータをQRコード画像に変換して認証しますが、 |
TOTPコードの計算方法
TOTPコード部で指定するワンタイムパスワードは、 RFC6238 で公開されている「時間ベースのワンタイムパスワードアルゴリズム」を使用して計算します。
|
RFC4226 で定義されているように、ハッシュ関数にはSHA-1を使用し、出力は6桁となるように切り捨てを行います。 |
TOTPコードの生成に必要なデータは現在時刻、TOTPシード、タイムステップの3つです。メンバーごとに設定されたTOTPシードとタイムステップを関数 generate_totp (generateTOTP) に引数として与えることで、TOTPコードを取得することができます。
TOTPコード生成の実装例(Python)
import hmac
import hashlib
import time
import base64
def generate_totp(totp_seed_b32: str, timestep: int) -> str:
"""
TOTP(Time-based One-Time Password)を生成
RFC 6238準拠の実装
"""
# Base32デコードして秘密鍵を取得
totp_seed = base64.b32decode(totp_seed_b32)
# 現在時刻からカウンター値を計算
current_step = int(time.time()) // timestep
counter = current_step.to_bytes(8, byteorder='big')
# HMAC-SHA1でハッシュを計算
hmac_result = hmac.new(totp_seed, counter, hashlib.sha1).digest()
# Dynamic Truncation: 最後のバイトの下位4ビットをオフセットとして使用
offset = hmac_result[19] & 0xf
# オフセット位置から4バイトを取得し31ビット整数を構成
otp = (
(hmac_result[offset] & 0x7f) << 24
| (hmac_result[offset + 1] & 0xff) << 16
| (hmac_result[offset + 2] & 0xff) << 8
| (hmac_result[offset + 3] & 0xff)
) % 1000000
# 6桁の文字列として返す
return str(otp).zfill(6)
if __name__ == '__main__':
# Base32エンコードされたシード(メンバー取得APIで得られるtotp_seed)
totp_seed_b32 = 'JH4MV7R7FV55TVB43FKSE5GNV2JRXXAL'
# タイムステップ(メンバー取得APIで得られるtimestep)
timestep = 30
totp_code = generate_totp(totp_seed_b32, timestep)
print(totp_code)
TOTPコード生成の実装例(JavaScript)
import { createHmac } from 'crypto';
/**
* Base32文字列をUint8Arrayにデコードする
*/
function base32Decode(input) {
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const normalizedInput = input.toUpperCase().replace(/=/g, "");
const fiveBitValues = [];
for (const char of normalizedInput) {
const alphabetIndex = BASE32_ALPHABET.indexOf(char);
if (alphabetIndex === -1) {
throw new Error(`Invalid Base32 character: '${char}'`);
}
fiveBitValues.push(alphabetIndex);
}
const decodedBytes = [];
let accumulator = 0;
let bitsInAccumulator = 0;
for (const fiveBits of fiveBitValues) {
accumulator = (accumulator << 5) | fiveBits;
bitsInAccumulator += 5;
while (bitsInAccumulator >= 8) {
const byte = (accumulator >> (bitsInAccumulator - 8)) & 0xFF;
decodedBytes.push(byte);
bitsInAccumulator -= 8;
}
}
return new Uint8Array(decodedBytes);
}
/**
* TOTP(Time-based One-Time Password)を生成
* RFC 6238準拠の実装
*/
function generateTotp(totpSeedB32, timestep) {
// Base32デコードして秘密鍵を取得
const totpSeed = base32Decode(totpSeedB32);
// 現在時刻からカウンター値を計算
const currentStep = Math.floor(Date.now() / 1000 / timestep);
const counterBytes = new Uint8Array(8);
const view = new DataView(counterBytes.buffer);
view.setBigUint64(0, BigInt(currentStep), false); // false = ビッグエンディアン
// HMAC-SHA1でハッシュを計算
const hmacResult = new Uint8Array(createHmac('sha1', totpSeed).update(counterBytes).digest());
// Dynamic Truncation: 最後のバイトの下位4ビットをオフセットとして使用
const offset = hmacResult[19] & 0xf;
// オフセット位置から4バイトを取得し31ビット整数を構成
const otp = (
(hmacResult[offset] & 0x7f) << 24
| (hmacResult[offset + 1] & 0xff) << 16
| (hmacResult[offset + 2] & 0xff) << 8
| (hmacResult[offset + 3] & 0xff)
) % 1000000;
// 6桁の文字列として返す
return otp.toString().padStart(6, '0');
}
function main() {
// Base32エンコードされたシード(メンバー取得APIで得られるtotp_seed)
const totpSeedB32 = 'JH4MV7R7FV55TVB43FKSE5GNV2JRXXAL';
// タイムステップ(メンバー取得APIで得られるtimestep)
const timestep = 30;
const totpCode = generateTotp(totpSeedB32, timestep);
console.log(totpCode);
}
main();
TOTPコード生成の実装例(Go)
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"time"
)
// generateTotp はTOTP(Time-based One-Time Password)を生成
// RFC 6238準拠の実装
func generateTotp(totpSeedB32 string, timestep int) (string, error) {
// Base32デコードして秘密鍵を取得
totpSeed, err := base32.StdEncoding.DecodeString(totpSeedB32)
if err != nil {
return "", err
}
// 現在時刻からカウンター値を計算
currentStep := time.Now().Unix() / int64(timestep)
counter := make([]byte, 8)
binary.BigEndian.PutUint64(counter, uint64(currentStep))
// HMAC-SHA1でハッシュを計算
h := hmac.New(sha1.New, totpSeed)
h.Write(counter)
hmacResult := h.Sum(nil)
// Dynamic Truncation: 最後のバイトの下位4ビットをオフセットとして使用
offset := hmacResult[19] & 0xf
// オフセット位置から4バイトを取得し31ビット整数を構成
otp := (int(hmacResult[offset]&0x7f) << 24 |
int(hmacResult[offset+1]&0xff) << 16 |
int(hmacResult[offset+2]&0xff) << 8 |
int(hmacResult[offset+3]&0xff)) % 1000000
// 6桁の文字列として返す
return fmt.Sprintf("%06d", otp), nil
}
func main() {
// Base32エンコードされたシード(メンバー取得APIで得られるtotp_seed)
totpSeedB32 := "JH4MV7R7FV55TVB43FKSE5GNV2JRXXAL"
// タイムステップ(メンバー取得APIで得られるtimestep)
timestep := 30
totpCode, err := generateTotp(totpSeedB32, timestep)
if err != nil {
panic(err)
}
fmt.Println(totpCode)
}
|
TOTPは広く一般的に知られるアルゴリズムで各種ライブラリも整っています。連携の際に既存のライブラリを使用して実装しても問題ありません。実装後は、ワンタイムQR取得APIのレスポンス結果と一致するか必ず検証してください。時刻同期のずれや実装の違いにより結果が異なる場合があります。 |
残り秒数の計算方法
ワンタイムQRが切り替わるまでの秒数、すなわち、現在のタイムステップにおける残り秒数はタイムステップから算出することができます。
残り秒数の計算の実装例(Python)
import time
def get_totp_expiration_in_seconds(timestep: int):
"""
TOTPの現在のタイムステップの残り秒数を計算する
"""
current_unix_time = int(time.time())
# 現在の時間窓内での経過秒数を計算
elapsed_seconds_in_current_window = current_unix_time % timestep
# 残り秒数 = タイムステップ - 経過秒数
expiration_in_seconds = timestep - elapsed_seconds_in_current_window
return expiration_in_seconds
if __name__ == '__main__':
# タイムステップ(メンバー取得APIで得られるtimestep)
timestep = 30
# 現在のタイムステップの残り秒数を出力
print(get_totp_expiration_in_seconds(timestep))
残り秒数の計算の実装例(JavaScript)
/**
* TOTPの現在のタイムステップの残り秒数を計算する
*/
function getTotpExpirationInSeconds(timestep) {
const currentUnixTime = Math.floor(Date.now() / 1000);
// 現在の時間窓内での経過秒数を計算
const elapsedSecondsInCurrentWindow = currentUnixTime % timestep;
// 残り秒数 = タイムステップ - 経過秒数
const expirationInSeconds = timestep - elapsedSecondsInCurrentWindow;
return expirationInSeconds;
}
// 使用例
function main() {
// タイムステップ(メンバー取得APIで得られるtimestep)
const timestep = 30;
// 現在のタイムステップの残り秒数を出力
console.log(getTotpExpirationInSeconds(timestep));
}
// 実行
main();
残り秒数の計算の実装例(Go)
package main
import (
"fmt"
"time"
)
// TOTPの現在のタイムステップの残り秒数を計算する
func getTotpExpirationInSeconds(timestep int64) int64 {
currentUnixTime := time.Now().Unix()
// 現在の時間窓内での経過秒数を計算
elapsedSecondsInCurrentWindow := currentUnixTime % timestep
// 残り秒数 = タイムステップ - 経過秒数
expirationInSeconds := timestep - elapsedSecondsInCurrentWindow
return expirationInSeconds
}
func main() {
// タイムステップ(メンバー取得APIで得られるtimestep)
timestep := int64(30)
// 現在のタイムステップの残り秒数を出力
fmt.Println(getTotpExpirationInSeconds(timestep))
}
|
残り秒数の取りうる値の範囲は、0秒~設定されたタイムステップの秒数となります。 |