From a4cdc02e5017ba587c169cb28a7e7927fc64c7cf Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 2 Nov 2021 10:12:52 -0300 Subject: [PATCH] totp qr code --- packages/anastasis-webui/package.json | 1 + packages/anastasis-webui/src/declaration.d.ts | 3 +- .../home/ReviewPoliciesScreen.stories.tsx | 2 +- .../AuthMethodTotpSetup.stories.tsx | 6 +- .../authMethodSetup/AuthMethodTotpSetup.tsx | 54 ++++++++++++++---- .../src/pages/home/authMethodSetup/totp.ts | 56 +++++++++++++++++++ pnpm-lock.yaml | 6 ++ 7 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 packages/anastasis-webui/src/pages/home/authMethodSetup/totp.ts diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index 2f2577a92..b032c563c 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -53,6 +53,7 @@ "eslint-config-preact": "^1.1.1", "jest": "^26.2.2", "jest-preset-preact": "^4.0.2", + "jssha": "^3.2.0", "preact-cli": "^3.2.2", "sass": "^1.32.13", "sass-loader": "^10.1.1", diff --git a/packages/anastasis-webui/src/declaration.d.ts b/packages/anastasis-webui/src/declaration.d.ts index edd3a07a3..2c4b7cb3a 100644 --- a/packages/anastasis-webui/src/declaration.d.ts +++ b/packages/anastasis-webui/src/declaration.d.ts @@ -17,5 +17,4 @@ declare module '*.png' { declare module 'jed' { const x: any; export = x; - } - \ No newline at end of file +} diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx index 007011326..5ba0c937d 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx @@ -244,5 +244,5 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, { type: "question", instructions: "Does P equal NP?", challenge: "C5SP8" - }] +}] } as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx index 3447e3d61..4e46b600e 100644 --- a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx @@ -45,7 +45,7 @@ export const WithOneExample = createExample(TestedComponent[type].screen, reduce configured: [{ challenge: 'qwe', type, - instructions: 'instr', + instructions: 'Enter 8 digits code for "Anastasis"', remove: () => null }] }); @@ -53,12 +53,12 @@ export const WithMoreExample = createExample(TestedComponent[type].screen, reduc configured: [{ challenge: 'qwe', type, - instructions: 'instr', + instructions: 'Enter 8 digits code for "Anastasis1"', remove: () => null },{ challenge: 'qwe', type, - instructions: 'instr', + instructions: 'Enter 8 digits code for "Anastasis2"', remove: () => null }] }); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx index bbffedad6..db656e630 100644 --- a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx @@ -4,36 +4,70 @@ import { stringToBytes } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { useMemo, useState } from "preact/hooks"; import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; import { AnastasisClientFrame } from "../index"; import { TextInput } from "../../../components/fields/TextInput"; 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({ authentication_method: { type: "totp", - instructions: `Enter code for ${name}`, - challenge: encodeCrock(stringToBytes(name)), + instructions: `Enter ${digits} digits code for ${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 (

- 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 + 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 with your TOTP App to import the TOTP secret into your TOTP App.

-
+
- +
+ +
+

+ After scanning the code with your TOTP App, test it in the input below. +

+ + {configured.length > 0 &&
+
+ Your TOTP numbers: +
+ {configured.map((c, i) => { + return
+

{c.instructions}

+
+
+ })} +
}
diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/totp.ts b/packages/anastasis-webui/src/pages/home/authMethodSetup/totp.ts new file mode 100644 index 000000000..0bc3feaf8 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/totp.ts @@ -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)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e38b0e856..e22c3067a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,7 @@ importers: jed: 1.1.1 jest: ^26.2.2 jest-preset-preact: ^4.0.2 + jssha: ^3.2.0 preact: ^10.3.1 preact-cli: ^3.2.2 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 jest: 26.6.3 jest-preset-preact: 4.0.2_9b3f24ae35a87c3c82fffbe3fdf70e1e + jssha: 3.2.0 preact-cli: 3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7 sass: 1.43.2 sass-loader: 10.2.0_sass@1.43.2 @@ -15313,6 +15315,10 @@ packages: verror: 1.10.0 dev: true + /jssha/3.2.0: + resolution: {integrity: sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==} + dev: true + /jsx-ast-utils/3.2.0: resolution: {integrity: sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==} engines: {node: '>=4.0'}