totp qr code

This commit is contained in:
Sebastian 2021-11-02 10:12:52 -03:00
parent 88d142d209
commit a4cdc02e50
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
7 changed files with 112 additions and 16 deletions

View File

@ -53,6 +53,7 @@
"eslint-config-preact": "^1.1.1", "eslint-config-preact": "^1.1.1",
"jest": "^26.2.2", "jest": "^26.2.2",
"jest-preset-preact": "^4.0.2", "jest-preset-preact": "^4.0.2",
"jssha": "^3.2.0",
"preact-cli": "^3.2.2", "preact-cli": "^3.2.2",
"sass": "^1.32.13", "sass": "^1.32.13",
"sass-loader": "^10.1.1", "sass-loader": "^10.1.1",

View File

@ -17,5 +17,4 @@ declare module '*.png' {
declare module 'jed' { declare module 'jed' {
const x: any; const x: any;
export = x; export = x;
} }

View File

@ -244,5 +244,5 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
type: "question", type: "question",
instructions: "Does P equal NP?", instructions: "Does P equal NP?",
challenge: "C5SP8" challenge: "C5SP8"
}] }]
} as ReducerState); } as ReducerState);

View File

@ -45,7 +45,7 @@ export const WithOneExample = createExample(TestedComponent[type].screen, reduce
configured: [{ configured: [{
challenge: 'qwe', challenge: 'qwe',
type, type,
instructions: 'instr', instructions: 'Enter 8 digits code for "Anastasis"',
remove: () => null remove: () => null
}] }]
}); });
@ -53,12 +53,12 @@ export const WithMoreExample = createExample(TestedComponent[type].screen, reduc
configured: [{ configured: [{
challenge: 'qwe', challenge: 'qwe',
type, type,
instructions: 'instr', instructions: 'Enter 8 digits code for "Anastasis1"',
remove: () => null remove: () => null
},{ },{
challenge: 'qwe', challenge: 'qwe',
type, type,
instructions: 'instr', instructions: 'Enter 8 digits code for "Anastasis2"',
remove: () => null remove: () => null
}] }]
}); });

View File

@ -4,36 +4,70 @@ import {
stringToBytes stringToBytes
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useMemo, useState } from "preact/hooks";
import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput"; import { TextInput } from "../../../components/fields/TextInput";
import { QR } from "../../../components/QR"; import { QR } from "../../../components/QR";
import { base32enc, computeTOTPandCheck } from "./totp";
export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
const [name, setName] = useState("anastasis");
const [test, setTest] = useState("");
const digits = 8
const secretKey = useMemo(() => {
const array = new Uint8Array(32)
return window.crypto.getRandomValues(array)
}, [])
const secret32 = base32enc(secretKey);
const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`
export function AuthMethodTotpSetup({addAuthMethod, cancel, configured}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("");
const addTotpAuth = (): void => addAuthMethod({ const addTotpAuth = (): void => addAuthMethod({
authentication_method: { authentication_method: {
type: "totp", type: "totp",
instructions: `Enter code for ${name}`, instructions: `Enter ${digits} digits code for ${name}`,
challenge: encodeCrock(stringToBytes(name)), challenge: encodeCrock(stringToBytes(totpURL)),
}, },
}); });
const errors = !name ? 'The TOTP name is missing' : undefined;
const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
const errors = !name ? 'The TOTP name is missing' : (
!testCodeMatches ? 'The test code doesnt match' : undefined
);
return ( return (
<AnastasisClientFrame hideNav title="Add TOTP authentication"> <AnastasisClientFrame hideNav title="Add TOTP authentication">
<p> <p>
For Time-based One-Time Password (TOTP) authentication, you need to set For Time-based One-Time Password (TOTP) authentication, you need to set
a name for the TOTP secret. Then, you must scan the generated QR code a name for the TOTP secret. Then, you must scan the generated QR code
with your TOTP App to import the TOTP secret into your TOTP App. with your TOTP App to import the TOTP secret into your TOTP App.
</p> </p>
<div> <div class="block">
<TextInput <TextInput
label="TOTP Name" label="TOTP Name"
grabFocus grabFocus
bind={[name, setName]} /> bind={[name, setName]} />
</div> </div>
<QR text={`sometext ${name}`} /> <div style={{ height: 300 }}>
<QR text={totpURL} />
</div>
<p>
After scanning the code with your TOTP App, test it in the input below.
</p>
<TextInput
label="Test code"
bind={[test, setTest]} />
{configured.length > 0 && <section class="section">
<div class="block">
Your TOTP numbers:
</div><div class="block">
{configured.map((c, i) => {
return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
<p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
<div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
</div>
})}
</div></section>}
<div> <div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={cancel}>Cancel</button> <button class="button" onClick={cancel}>Cancel</button>

View File

@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/camelcase */
import jssha from 'jssha'
const SEARCH_RANGE = 16
const timeStep = 30
export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean {
const now = new Date().getTime()
const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
const movingFactor = (epoch + ms).toString(16).padStart(16, "0");
const hmacSha = new jssha('SHA-1', 'HEX', { hmacKey: { value: secretKey, format: 'UINT8ARRAY' } });
hmacSha.update(movingFactor);
const hmac_text = hmacSha.getHMAC('UINT8ARRAY');
const offset = (hmac_text[hmac_text.length - 1] & 0xf)
const otp = ((
(hmac_text[offset + 0] << 24) +
(hmac_text[offset + 1] << 16) +
(hmac_text[offset + 2] << 8) +
(hmac_text[offset + 3])
) & 0x7fffffff) % Math.pow(10, digits)
if (otp == code) return true
}
return false
}
const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split('')
export function base32enc(buffer: Uint8Array): string {
let rpos = 0
let bits = 0
let vbit = 0
let result = ""
while ((rpos < buffer.length) || (vbit > 0)) {
if ((rpos < buffer.length) && (vbit < 5)) {
bits = (bits << 8) | buffer[rpos++];
vbit += 8;
}
if (vbit < 5) {
bits <<= (5 - vbit);
vbit = 5;
}
result += encTable__[(bits >> (vbit - 5)) & 31];
vbit -= 5;
}
return result
}
// const array = new Uint8Array(256)
// const secretKey = window.crypto.getRandomValues(array)
// console.log(base32enc(secretKey))

View File

@ -57,6 +57,7 @@ importers:
jed: 1.1.1 jed: 1.1.1
jest: ^26.2.2 jest: ^26.2.2
jest-preset-preact: ^4.0.2 jest-preset-preact: ^4.0.2
jssha: ^3.2.0
preact: ^10.3.1 preact: ^10.3.1
preact-cli: ^3.2.2 preact-cli: ^3.2.2
preact-render-to-string: ^5.1.4 preact-render-to-string: ^5.1.4
@ -96,6 +97,7 @@ importers:
eslint-config-preact: 1.1.4_eslint@6.8.0+typescript@3.9.10 eslint-config-preact: 1.1.4_eslint@6.8.0+typescript@3.9.10
jest: 26.6.3 jest: 26.6.3
jest-preset-preact: 4.0.2_9b3f24ae35a87c3c82fffbe3fdf70e1e jest-preset-preact: 4.0.2_9b3f24ae35a87c3c82fffbe3fdf70e1e
jssha: 3.2.0
preact-cli: 3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7 preact-cli: 3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7
sass: 1.43.2 sass: 1.43.2
sass-loader: 10.2.0_sass@1.43.2 sass-loader: 10.2.0_sass@1.43.2
@ -15313,6 +15315,10 @@ packages:
verror: 1.10.0 verror: 1.10.0
dev: true dev: true
/jssha/3.2.0:
resolution: {integrity: sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==}
dev: true
/jsx-ast-utils/3.2.0: /jsx-ast-utils/3.2.0:
resolution: {integrity: sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==} resolution: {integrity: sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}