2026-08-21 08:45:25 +08:00
|
|
|
package simcu::otp
|
2026-08-21 08:43:24 +08:00
|
|
|
|
|
|
|
|
import std.random.*
|
|
|
|
|
import std.convert.*
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* otpauth:// URI 生成(Google Authenticator / 常见 OTP App 标准)。
|
|
|
|
|
* 格式:otpauth://totp/{issuer}:{account}?secret={base32}&issuer={issuer}&algorithm=SHA1&digits=6&period=30
|
|
|
|
|
* 同时提供随机 Base32 secret 生成工具。
|
|
|
|
|
*/
|
|
|
|
|
public class OtpUri {
|
|
|
|
|
/// 生成随机 Base32 secret(默认 20 字节 = 160 bit,与 RFC 4226 密钥长度一致)
|
|
|
|
|
public static func generateSecret(): String {
|
|
|
|
|
generateSecret(20)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 生成指定字节数的随机 Base32 secret
|
|
|
|
|
public static func generateSecret(bytes: Int64): String {
|
|
|
|
|
let rnd = Random()
|
|
|
|
|
let data = Array<Byte>(bytes, repeat: 0)
|
|
|
|
|
for (i in 0..bytes) {
|
|
|
|
|
data[i] = UInt8(rnd.nextUInt64() & 0xff)
|
|
|
|
|
}
|
|
|
|
|
Base32.encode(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 生成 otpauth:// 链接(默认 6 位 / 30s 步长)
|
|
|
|
|
public static func build(issuer: String, account: String, secretBase32: String): String {
|
|
|
|
|
build(issuer, account, secretBase32, 6, 30)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 生成 otpauth:// 链接(指定位数)
|
|
|
|
|
public static func build(issuer: String, account: String, secretBase32: String, digits: Int64): String {
|
|
|
|
|
build(issuer, account, secretBase32, digits, 30)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 生成 otpauth:// 链接(指定位数与步长)
|
|
|
|
|
public static func build(issuer: String, account: String, secretBase32: String, digits: Int64, period: Int64): String {
|
|
|
|
|
let label = issuer + ":" + account
|
|
|
|
|
var sb = StringBuilder()
|
|
|
|
|
sb.append("otpauth://totp/")
|
|
|
|
|
sb.append(urlEncode(label))
|
|
|
|
|
sb.append("?secret=")
|
|
|
|
|
sb.append(secretBase32)
|
|
|
|
|
sb.append("&issuer=")
|
|
|
|
|
sb.append(urlEncode(issuer))
|
|
|
|
|
sb.append("&algorithm=SHA1&digits=")
|
|
|
|
|
sb.append(digits.toString())
|
|
|
|
|
sb.append("&period=")
|
|
|
|
|
sb.append(period.toString())
|
|
|
|
|
sb.toString()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func urlEncode(s: String): String {
|
|
|
|
|
var sb = StringBuilder()
|
|
|
|
|
for (ch in s) {
|
|
|
|
|
match (ch) {
|
|
|
|
|
case ':' => sb.append("%3A")
|
|
|
|
|
case '/' => sb.append("%2F")
|
|
|
|
|
case ' ' => sb.append("%20")
|
|
|
|
|
case _ => sb.append(Rune(ch))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sb.toString()
|
|
|
|
|
}
|
|
|
|
|
}
|