anastasis-webui: make TOTP work again

This commit is contained in:
Florian Dold 2022-04-13 19:32:12 +02:00
parent ec9aed276a
commit 5054ff6c6d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
11 changed files with 93 additions and 42 deletions

View File

@ -14,7 +14,6 @@ import {
getRandomBytes, getRandomBytes,
hash, hash,
HttpStatusCode, HttpStatusCode,
j2s,
Logger, Logger,
parsePayUri, parsePayUri,
stringToBytes, stringToBytes,
@ -27,8 +26,7 @@ import {
import { anastasisData } from "./anastasis-data.js"; import { anastasisData } from "./anastasis-data.js";
import { import {
EscrowConfigurationResponse, EscrowConfigurationResponse,
IbanExternalAuthResponse, RecoveryMetaResponse,
RecoveryMetaResponse as RecoveryMetaResponse,
TruthUploadRequest, TruthUploadRequest,
} from "./provider-types.js"; } from "./provider-types.js";
import { import {

View File

@ -15,6 +15,7 @@
"pretty": "prettier --write src", "pretty": "prettier --write src",
"storybook": "start-storybook -p 6006" "storybook": "start-storybook -p 6006"
}, },
"type": "module",
"eslintConfig": { "eslintConfig": {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": [

View File

@ -28,7 +28,7 @@ const commitHash = cp.execSync("git rev-parse --short HEAD").toString();
export default { export default {
webpack(config, env, helpers) { webpack(config, env, helpers) {
// add __VERSION__ to be use in the html // add __VERSION__ to be used in the html
config.plugins.push( config.plugins.push(
new DefinePlugin({ new DefinePlugin({
"process.env.__VERSION__": JSON.stringify( "process.env.__VERSION__": JSON.stringify(

View File

@ -23,7 +23,7 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import langIcon from "../../assets/icons/languageicon.svg"; import langIcon from "../../assets/icons/languageicon.svg";
import { useTranslationContext } from "../../context/translation"; import { useTranslationContext } from "../../context/translation";
import { strings as messages } from "../../i18n/strings"; import { strings as messages } from "../../i18n/strings.js";
type LangsNames = { type LangsNames = {
[P in keyof typeof messages]: string; [P in keyof typeof messages]: string;

View File

@ -78,8 +78,7 @@ export function Sidebar({ mobile }: Props): VNode {
</div> </div>
</li> </li>
)} )}
{reducer.currentReducerState && {reducer.currentReducerState?.reducer_type === "backup" ? (
reducer.currentReducerState.backup_state ? (
<Fragment> <Fragment>
<li <li
class={ class={
@ -191,8 +190,7 @@ export function Sidebar({ mobile }: Props): VNode {
</li> </li>
</Fragment> </Fragment>
) : ( ) : (
reducer.currentReducerState && reducer.currentReducerState?.reducer_type === "recovery" && (
reducer.currentReducerState?.recovery_state && (
<Fragment> <Fragment>
<li <li
class={ class={

View File

@ -199,7 +199,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
s = await reduceAction(anastasisState.reducerState!, action, args); s = await reduceAction(anastasisState.reducerState!, action, args);
} }
console.log("got response from reducer", s); console.log("got response from reducer", s);
if (s.code) { if (s.reducer_type === "error") {
console.log("response is an error"); console.log("response is an error");
setAnastasisState({ ...anastasisState, currentError: s }); setAnastasisState({ ...anastasisState, currentError: s });
} else { } else {
@ -223,7 +223,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
} else { } else {
s = await getBackupStartState(); s = await getBackupStartState();
} }
if (s.code !== undefined) { if (s.reducer_type === "error") {
setAnastasisState({ setAnastasisState({
...anastasisState, ...anastasisState,
currentError: s, currentError: s,
@ -274,7 +274,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
} else { } else {
s = await getRecoveryStartState(); s = await getRecoveryStartState();
} }
if (s.code !== undefined) { if (s.reducer_type === "error") {
setAnastasisState({ setAnastasisState({
...anastasisState, ...anastasisState,
currentError: s, currentError: s,
@ -296,8 +296,10 @@ export function useAnastasisReducer(): AnastasisReducerApi {
return; return;
} }
if ( if (
reducerState.backup_state === BackupStates.ContinentSelecting || (reducerState.reducer_type === "backup" &&
reducerState.recovery_state === RecoveryStates.ContinentSelecting reducerState.backup_state === BackupStates.ContinentSelecting) ||
(reducerState.reducer_type === "recovery" &&
reducerState.recovery_state === RecoveryStates.ContinentSelecting)
) { ) {
setAnastasisState({ setAnastasisState({
...anastasisState, ...anastasisState,
@ -327,7 +329,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
} }
const s = txHandle.transactionState; const s = txHandle.transactionState;
console.log("transaction finished, new state", s); console.log("transaction finished, new state", s);
if (s.code !== undefined) { if (s.reducer_type === "error") {
setAnastasisState({ setAnastasisState({
...anastasisState, ...anastasisState,
currentError: txHandle.transactionState, currentError: txHandle.transactionState,
@ -355,7 +357,7 @@ class ReducerTxImpl implements ReducerTransactionHandle {
console.log("making transition in transaction", action); console.log("making transition in transaction", action);
this.transactionState = s; this.transactionState = s;
// Abort transaction as soon as we transition into an error state. // Abort transaction as soon as we transition into an error state.
if (this.transactionState.code !== undefined) { if (this.transactionState.reducer_type === "error") {
throw Error("transition resulted in error"); throw Error("transition resulted in error");
} }
return this.transactionState; return this.transactionState;

View File

@ -1,6 +1,6 @@
import { UserAttributeSpec, validators } from "@gnu-taler/anastasis-core"; import { UserAttributeSpec, validators } from "@gnu-taler/anastasis-core";
import { isAfter, parse } from "date-fns"; import { isAfter, parse } from "date-fns";
import { Fragment, h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { DateInput } from "../../components/fields/DateInput"; import { DateInput } from "../../components/fields/DateInput";
import { PhoneNumberInput } from "../../components/fields/NumberInput"; import { PhoneNumberInput } from "../../components/fields/NumberInput";
@ -72,7 +72,10 @@ export function AttributeEntryScreen(): VNode {
const doConfirm = async () => { const doConfirm = async () => {
await reducer.transition("enter_user_attributes", { await reducer.transition("enter_user_attributes", {
identity_attributes: attrs, identity_attributes: {
application_id: "anastasis-standalone",
...attrs,
},
}); });
}; };

View File

@ -7,6 +7,11 @@ import { TextInput } from "../../../components/fields/TextInput";
import { QR } from "../../../components/QR"; import { QR } from "../../../components/QR";
import { base32enc, computeTOTPandCheck } from "./totp"; import { base32enc, computeTOTPandCheck } from "./totp";
/**
* This is hard-coded in the protocol for TOTP auth.
*/
const ANASTASIS_TOTP_DIGITS = 8;
export function AuthMethodTotpSetup({ export function AuthMethodTotpSetup({
addAuthMethod, addAuthMethod,
cancel, cancel,
@ -14,20 +19,20 @@ export function AuthMethodTotpSetup({
}: AuthMethodSetupProps): VNode { }: AuthMethodSetupProps): VNode {
const [name, setName] = useState("anastasis"); const [name, setName] = useState("anastasis");
const [test, setTest] = useState(""); const [test, setTest] = useState("");
const digits = 8;
const secretKey = useMemo(() => { const secretKey = useMemo(() => {
const array = new Uint8Array(32); const array = new Uint8Array(32);
return window.crypto.getRandomValues(array); return window.crypto.getRandomValues(array);
}, []); }, []);
const secret32 = base32enc(secretKey); const secret32 = base32enc(secretKey);
const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`; const totpURL = `otpauth://totp/${name}?digits=${ANASTASIS_TOTP_DIGITS}&secret=${secret32}`;
const addTotpAuth = (): void => const addTotpAuth = (): void =>
addAuthMethod({ addAuthMethod({
authentication_method: { authentication_method: {
type: "totp", type: "totp",
instructions: `Enter ${digits} digits code for "${name}"`, instructions: `Enter ${ANASTASIS_TOTP_DIGITS} digits code for "${name}"`,
challenge: encodeCrock(stringToBytes(totpURL)), challenge: encodeCrock(secretKey),
}, },
}); });

View File

@ -11,7 +11,7 @@ import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode { export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode {
const [answerCode, setAnswerCode] = useState(""); const [answerCode, setAnswerCode] = useState("");
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
@ -74,7 +74,7 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
async function onNext(): Promise<void> { async function onNext(): Promise<void> {
console.log(`sending TOTP code '${answerCode}'`); console.log(`sending TOTP code '${answerCode}'`);
return reducer?.transition("solve_challenge", { return reducer?.transition("solve_challenge", {
pin: Number.parseInt(answerCode), answer: answerCode,
}); });
} }
function onCancel(): void { function onCancel(): void {

View File

@ -34,7 +34,7 @@ import { StartScreen } from "./StartScreen";
import { TruthsPayingScreen } from "./TruthsPayingScreen"; import { TruthsPayingScreen } from "./TruthsPayingScreen";
function isBackup(reducer: AnastasisReducerApi): boolean { function isBackup(reducer: AnastasisReducerApi): boolean {
return !!reducer.currentReducerState?.backup_state; return reducer.currentReducerState?.reducer_type === "backup";
} }
export function withProcessLabel( export function withProcessLabel(
@ -171,57 +171,96 @@ function AnastasisClientImpl(): VNode {
console.log("state", reducer.currentReducerState); console.log("state", reducer.currentReducerState);
if ( if (
state.backup_state === BackupStates.ContinentSelecting || (state.reducer_type === "backup" &&
state.recovery_state === RecoveryStates.ContinentSelecting || state.backup_state === BackupStates.ContinentSelecting) ||
state.backup_state === BackupStates.CountrySelecting || (state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.CountrySelecting state.recovery_state === RecoveryStates.ContinentSelecting) ||
(state.reducer_type === "backup" &&
state.backup_state === BackupStates.CountrySelecting) ||
(state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.CountrySelecting)
) { ) {
return <ContinentSelectionScreen />; return <ContinentSelectionScreen />;
} }
if ( if (
state.backup_state === BackupStates.UserAttributesCollecting || (state.reducer_type === "backup" &&
state.recovery_state === RecoveryStates.UserAttributesCollecting state.backup_state === BackupStates.UserAttributesCollecting) ||
(state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.UserAttributesCollecting)
) { ) {
return <AttributeEntryScreen />; return <AttributeEntryScreen />;
} }
if (state.backup_state === BackupStates.AuthenticationsEditing) { if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.AuthenticationsEditing
) {
return <AuthenticationEditorScreen />; return <AuthenticationEditorScreen />;
} }
if (state.backup_state === BackupStates.PoliciesReviewing) { if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.PoliciesReviewing
) {
return <ReviewPoliciesScreen />; return <ReviewPoliciesScreen />;
} }
if (state.backup_state === BackupStates.SecretEditing) { if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.SecretEditing
) {
return <SecretEditorScreen />; return <SecretEditorScreen />;
} }
if (state.backup_state === BackupStates.BackupFinished) { if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.BackupFinished
) {
return <BackupFinishedScreen />; return <BackupFinishedScreen />;
} }
if (state.backup_state === BackupStates.TruthsPaying) { if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.TruthsPaying
) {
return <TruthsPayingScreen />; return <TruthsPayingScreen />;
} }
if (state.backup_state === BackupStates.PoliciesPaying) { if (
state.reducer_type === "backup" &&
state.backup_state === BackupStates.PoliciesPaying
) {
return <PoliciesPayingScreen />; return <PoliciesPayingScreen />;
} }
if (state.recovery_state === RecoveryStates.SecretSelecting) { if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.SecretSelecting
) {
return <SecretSelectionScreen />; return <SecretSelectionScreen />;
} }
if (state.recovery_state === RecoveryStates.ChallengeSelecting) { if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ChallengeSelecting
) {
return <ChallengeOverviewScreen />; return <ChallengeOverviewScreen />;
} }
if (state.recovery_state === RecoveryStates.ChallengeSolving) { if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ChallengeSolving
) {
return <SolveScreen />; return <SolveScreen />;
} }
if (state.recovery_state === RecoveryStates.RecoveryFinished) { if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.RecoveryFinished
) {
return <RecoveryFinishedScreen />; return <RecoveryFinishedScreen />;
} }
if (state.recovery_state === RecoveryStates.ChallengePaying) { if (
state.reducer_type === "recovery" &&
state.recovery_state === RecoveryStates.ChallengePaying
) {
return <ChallengePayingScreen />; return <ChallengePayingScreen />;
} }
console.log("unknown state", reducer.currentReducerState); console.log("unknown state", reducer.currentReducerState);

View File

@ -17,6 +17,11 @@ export function createExample<Props>(
<AnastasisProvider <AnastasisProvider
value={{ value={{
currentReducerState, currentReducerState,
discoverMore: async () => {},
discoverStart: async () => {},
discoveryState: {
state: "none",
},
currentError: undefined, currentError: undefined,
back: async () => { back: async () => {
null; null;