|
@@ -29,6 +29,15 @@ export interface Code {
|
|
* code remains valid.
|
|
* code remains valid.
|
|
*/
|
|
*/
|
|
period: number;
|
|
period: number;
|
|
|
|
+ /** The (HMAC) algorithm used by the OTP generator. */
|
|
|
|
+ algorithm: "sha1" | "sha256" | "sha512";
|
|
|
|
+ /**
|
|
|
|
+ * HOTP counter.
|
|
|
|
+ *
|
|
|
|
+ * Only valid for HOTP codes. It might be even missing for HOTP codes, in
|
|
|
|
+ * which case we should start from 0.
|
|
|
|
+ */
|
|
|
|
+ counter?: number;
|
|
/**
|
|
/**
|
|
* The secret that is used to drive the OTP generator.
|
|
* The secret that is used to drive the OTP generator.
|
|
*
|
|
*
|
|
@@ -36,8 +45,6 @@ export interface Code {
|
|
* {@link type}-specific manner).
|
|
* {@link type}-specific manner).
|
|
*/
|
|
*/
|
|
secret: string;
|
|
secret: string;
|
|
- /** The (HMAC) algorithm used by the OTP generator. */
|
|
|
|
- algorithm: "sha1" | "sha256" | "sha512";
|
|
|
|
/** The original string from which this code was generated. */
|
|
/** The original string from which this code was generated. */
|
|
uriString: string;
|
|
uriString: string;
|
|
}
|
|
}
|
|
@@ -53,6 +60,12 @@ export interface Code {
|
|
* - (TOTP)
|
|
* - (TOTP)
|
|
* otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM
|
|
* otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM
|
|
*
|
|
*
|
|
|
|
+ * - (HOTP)
|
|
|
|
+ * otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0
|
|
|
|
+ *
|
|
|
|
+ * - (Steam)
|
|
|
|
+ * otpauth://steam/Steam:SteamAccount?algorithm=SHA1&digits=5&issuer=Steam&period=30&secret=AAABBBCCCDDDEEEFFF
|
|
|
|
+ *
|
|
* See also `auth/test/models/code_test.dart`.
|
|
* See also `auth/test/models/code_test.dart`.
|
|
*/
|
|
*/
|
|
export const codeFromURIString = (id: string, uriString: string): Code => {
|
|
export const codeFromURIString = (id: string, uriString: string): Code => {
|
|
@@ -94,8 +107,9 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
|
|
issuer: parseIssuer(url, path),
|
|
issuer: parseIssuer(url, path),
|
|
length: parseLength(url, type),
|
|
length: parseLength(url, type),
|
|
period: parsePeriod(url),
|
|
period: parsePeriod(url),
|
|
- secret: parseSecret(url),
|
|
|
|
algorithm: parseAlgorithm(url),
|
|
algorithm: parseAlgorithm(url),
|
|
|
|
+ counter: parseCounter(url),
|
|
|
|
+ secret: parseSecret(url),
|
|
uriString,
|
|
uriString,
|
|
};
|
|
};
|
|
};
|
|
};
|
|
@@ -164,6 +178,11 @@ const parseAlgorithm = (url: URL): Code["algorithm"] => {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+const parseCounter = (url: URL): number | undefined => {
|
|
|
|
+ const c = url.searchParams.get("counter");
|
|
|
|
+ return c ? parseInt(c, 10) : undefined;
|
|
|
|
+};
|
|
|
|
+
|
|
const parseSecret = (url: URL): string =>
|
|
const parseSecret = (url: URL): string =>
|
|
ensure(url.searchParams.get("secret")).replaceAll(" ", "").toUpperCase();
|
|
ensure(url.searchParams.get("secret")).replaceAll(" ", "").toUpperCase();
|
|
|
|
|
|
@@ -194,13 +213,14 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
|
|
}
|
|
}
|
|
|
|
|
|
case "hotp": {
|
|
case "hotp": {
|
|
|
|
+ const counter = code.counter || 0;
|
|
const hotp = new HOTP({
|
|
const hotp = new HOTP({
|
|
secret: code.secret,
|
|
secret: code.secret,
|
|
- counter: 0,
|
|
|
|
|
|
+ counter: counter,
|
|
algorithm: code.algorithm,
|
|
algorithm: code.algorithm,
|
|
});
|
|
});
|
|
- otp = hotp.generate();
|
|
|
|
- nextOTP = hotp.generate({ counter: 1 });
|
|
|
|
|
|
+ otp = hotp.generate({ counter });
|
|
|
|
+ nextOTP = hotp.generate({ counter: counter + 1 });
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|