HOTP 和 TOTP 就是手机身份验证器应用所显示的验证码。这两个函数都位于 sgcCrypto_OTP 单元中, 并基于 HMAC 构建(参见哈希与消息认证)。
HOTP(基于计数器)
验证码取决于一个共享密钥和一个每次使用都递增 1 的计数器。客户端和服务器必须保持同步: 如果客户端生成了服务器从未见过的验证码(例如在没有网络连接的情况下按下了按钮), 两边的计数器就会产生偏移,此后客户端显示的每一个验证码都会失败,直到服务器重新同步为止。 正是这种偏移问题,导致大多数消费级身份验证器应用都改用 TOTP 取代了 HOTP。
function sgcHOTP(const aSecret: TBytes; aCounter: Int64; aDigits: Integer = 6): string;
TOTP(基于时间)
HOTP 中的计数器被替换为当前 Unix 时间按固定步长(默认 30 秒)划分后的结果, 因此客户端和服务器只需要大致同步的时钟,而不需要同步的计数器。这正是每一款身份验证器应用所实现的方式。
function sgcTOTP(const aSecret: TBytes; aUnixTime: Int64; aDigits: Integer = 6;
aStep: Integer = 30; aHash: TsgcKDFHash = khSHA1): string;
{ Takes the base32 secret exactly as an authenticator app displays or scans it. }
function sgcTOTP_FromBase32(const aSecretBase32: string; aUnixTime: Int64;
aDigits: Integer = 6; aStep: Integer = 30; aHash: TsgcKDFHash = khSHA1): string;
验证验证码
aWindow 会接受当前步长前后这么多个步长范围内的验证码,用以吸收轻微的时钟偏差, 或者用户输入验证码时动作较慢的情况。内部比较采用恒定时间实现,因此不会泄露猜测中有多少位数字是正确的。
function sgcTOTP_Verify(const aSecret: TBytes; const aCode: string; aUnixTime: Int64;
aWindow: Integer = 1; aDigits: Integer = 6; aStep: Integer = 30; aHash: TsgcKDFHash = khSHA1): Boolean;
var
oSecret: TBytes;
begin
oSecret := sgcRandomBytes(20);
if sgcTOTP_Verify(oSecret, edtCode.Text, DateTimeToUnix(Now, False)) then
{ accepted };
end;
配置密钥
身份验证器应用的配置方式是扫描一个编码了 otpauth:// URI 的二维码。其中的密钥采用 Base32 编码(参见密码学总览页中的 sgcBase32Encode):
var
oSecret: TBytes;
vURI: string;
begin
oSecret := sgcRandomBytes(20);
vURI := 'otpauth://totp/MyApp:user@example.com?secret=' + sgcBase32Encode(oSecret) +
'&issuer=MyApp&digits=6&period=30';
{ render vURI as a QR code for the user to scan }
end;