totp qr code
This commit is contained in:
parent
88d142d209
commit
a4cdc02e50
@ -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",
|
||||||
|
@ -17,5 +17,4 @@ declare module '*.png' {
|
|||||||
declare module 'jed' {
|
declare module 'jed' {
|
||||||
const x: any;
|
const x: any;
|
||||||
export = x;
|
export = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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))
|
@ -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'}
|
||||||
|
Loading…
Reference in New Issue
Block a user