std/otp
import std/otp to use it. Source: src/std/otp.iyi at v0.18.0.
import std/otp::{HOTP, TOTP, OTPAuth, OTPAlgorithm, OTPError} HOTP.code(secret, 1_i64) # "287082"
TOTP.code(secret, 59_i64, 30, 8) # "94287082"
TOTP.now(secret) # by the clock
TOTP.verify(secret, "287082", unix_time) # true or false
OTPAuth.totp(secret, "[email protected]", "Example").to_s
OTPAuth.parse(uri) # an OTPAuth or an OTPErrorA secret is an Array(UInt8), never a String: a secret is handed around as base32 text, and a String here could be read as that text or as its bytes, so a key typed into the wrong one would make codes that match nothing and say nothing. Base32.decode (std/base32) turns the text into the bytes, and OTPAuth.parse does it for a URI's secret.
HOTP is RFC 4226 section 5.3: HMAC of the counter's eight bytes, big-endian, under the secret; four bytes at the offset the last byte's low nibble names ("dynamic truncation"), their top bit cleared; that number modulo 10^digits, written with its leading zeros. TOTP is HOTP with the counter the Unix time divided by the step, T0 = 0 (RFC 6238 section 4). Both take SHA-1, SHA-256 or SHA-512 (OTPAlgorithm), the three RFC 6238 names, on std/crypto's HMAC. Digits run from 6, RFC 4226's floor (R-4), to 10, past which the 31-bit number has no more to give; authenticator apps read 6 and 8.
TOTP.verify takes the code from the steps window either side of unix_time as well, RFC 6238 section 5.2's allowance for a clock that is off and a user who is slow; the default is one step each way, the most that section recommends. Every candidate in the window is computed and compared with std/crypto's Subtle.constant_time_compare, all of them, so the time a guess takes does not say whether it was near. What it does not do is remember: section 5.2 says a code once accepted must not be accepted again, and that needs the last step a user was let in at, which is the caller's to keep.
The clock, and why this module imports std/time: TOTP.now is what a server calls, every time, and a TOTP module that cannot say what the code is now is half of one. So std/otp reaches std/time, and through it the wall clock and the environment (TZ, ZONEINFO), as std/cookie does for the same reason; iyi mod reach counts that for every program importing this. Everything else is a pure function of its arguments: HOTP.code of a counter, TOTP.code and TOTP.verify of an explicit Unix time, which is how the RFCs' vectors are checked and how a program with a clock of its own (a test, a replay) calls them; TOTP.now is the one call that reads the clock. Splitting now into a module of its own would make a module and a gate for a line. std/base32 is the split that matters: it is a codec other code needs without any of this, and it reaches nothing.
OTPAuth builds and reads otpauth://totp/Issuer:account?secret=.... Built: the label is the issuer, a colon and the account, each percent-encoded (everything but RFC 3986's unreserved characters), and the parameters are secret (base32, upper case, no padding, as the format asks), issuer, counter for HOTP, then algorithm, digits and period only when they are not the defaults SHA1, 6 and 30, so the common URI is the short one Google's own example shows. Read: the scheme and type in any case; %XX escapes in the label and values, + as a space in values; the account after the label's first colon (written or %3A), leading spaces dropped; parameters the format does not name (image) passed over. An issuer in the label and one in the parameters must agree.
Refused with a sentence, as misuse: an empty secret, digits outside 6 to 10, a step or period below one, a negative counter or Unix time, a negative window, a colon in an issuer or account being built (the format gives a colon no escape). Refused as an OTPError, because a URI is data: anything else parse cannot read, each naming what.
HOTPstruct
pub struct HOTP
def self.code(secret : Array(UInt8), counter : Int64, digits : Int32 = 6, algorithm : OTPAlgorithm = OTPAlgorithm::SHA1) : String
def initialize
endOTPAlgorithmenum
The HMAC hash, by the name RFC 6238 and the key URI format give it.
pub enum OTPAlgorithm : Int32
SHA1 = 0
SHA256 = 1
SHA512 = 2
endOTPAuthstruct
A key URI: what an authenticator app needs to make the codes a server will check. counter is nil for TOTP and the next counter for HOTP; period is TOTP's step and 30, unused, for HOTP.
pub struct OTPAuth
def account : String
def algorithm : OTPAlgorithm
def counter : Int64 | ::Nil
def digits : Int32
def self.hotp(secret : Array(UInt8), account : String, counter : Int64, issuer : String = "", algorithm : OTPAlgorithm = OTPAlgorithm::SHA1, digits : Int32 = 6) : OTPAuth
def hotp? : Bool
def initialize(secret : Array(UInt8), account : String, issuer : String, algorithm : OTPAlgorithm, digits : Int32, period : Int32, counter : Int64 | ::Nil)
def issuer : String
def self.parse(uri : String) : OTPAuth | OTPError
def period : Int32
def secret : Array(UInt8)
def to_s : String
# An empty *issuer* is none: the label is the account alone and no
# `issuer` parameter is written.
def self.totp(secret : Array(UInt8), account : String, issuer : String = "", algorithm : OTPAlgorithm = OTPAlgorithm::SHA1, digits : Int32 = 6, period : Int32 = 30) : OTPAuth
endOTPErrorstruct
What OTPAuth.parse answers for a URI it cannot read.
pub struct OTPError
def initialize(message : String)
def to_s : String
endTOTPstruct
pub struct TOTP
def self.code(secret : Array(UInt8), unix_time : Int64, step : Int32 = 30, digits : Int32 = 6, algorithm : OTPAlgorithm = OTPAlgorithm::SHA1) : String
def initialize
# The code for this second by the wall clock.
def self.now(secret : Array(UInt8), step : Int32 = 30, digits : Int32 = 6, algorithm : OTPAlgorithm = OTPAlgorithm::SHA1) : String
# Whether *code* is the code for *unix_time*'s step or one of the
# *window* steps either side of it. A code of the wrong length is false
# at once; the length of the right one is not a secret.
def self.verify(secret : Array(UInt8), code : String, unix_time : Int64, window : Int32 = 1, step : Int32 = 30, digits : Int32 = 6, algorithm : OTPAlgorithm = OTPAlgorithm::SHA1) : Bool
endError for OTPErrorimpl
impl Error for OTPError
def message : String
end
One-time passwords, written in iyi: HOTP (RFC 4226), TOTP (RFC 6238), and the
otpauth://key URI an authenticator app reads from a QR code (Google Authenticator's Key Uri Format, which is the de facto spec: https://github.com/google/google-authenticator/wiki/Key-Uri-Format).