1+ import React , {
2+ createContext , useContext , useEffect , useState , useCallback , ReactNode
3+ } from 'react' ;
4+ import {
5+ startRegistration ,
6+ startAuthentication ,
7+ deriveKey ,
8+ base64URLStringToBuffer ,
9+ } from '@/webauthn' ;
10+ import { encryptData , decryptData } from '@/webauthn' ;
11+
12+ type SecureContext < T > = {
13+ isAuthenticated : boolean ;
14+ encryptionKey : CryptoKey | null ;
15+ values : T | null ;
16+ login : ( ) => Promise < void > ;
17+ logout : ( ) => void ;
18+ setValues : ( v :T ) => Promise < void > ;
19+ } ;
20+
21+ const Ctx = createContext < SecureContext < any > | undefined > ( undefined ) ;
22+
23+ type FallbackRender < T > = ( props : {
24+ submit : ( values :T ) => Promise < void > ;
25+ error ? : string ;
26+ isLoading ? : boolean ;
27+ } ) => ReactNode ;
28+
29+ interface Props < T > {
30+ storageKey : string ;
31+ fallback : FallbackRender < T > ;
32+ children : ( ctx : SecureContext < T > ) => ReactNode ;
33+ }
34+
35+ export function SecureFormProvider < T extends Record < string , any > > (
36+ { storageKey, fallback, children } : Props < T >
37+ ) {
38+ const [ encryptionKey , setKey ] = useState < CryptoKey | null > ( null ) ;
39+ const [ values , setVals ] = useState < T | null > ( null ) ;
40+ const [ error , setErr ] = useState < string | null > ( null ) ;
41+ const [ isLoading , setLoad ] = useState ( false ) ;
42+
43+ /* ──────────────────────────────────────────────── */
44+ /* Helpers */
45+ /* ──────────────────────────────────────────────── */
46+
47+ const decryptFromStorage = useCallback ( async ( key :CryptoKey ) => {
48+ const cipher = localStorage . getItem ( `${ storageKey } .data` ) ;
49+ if ( ! cipher ) return null ;
50+ const json = await decryptData ( key , cipher ) ;
51+ return JSON . parse ( json ) as T ;
52+ } , [ storageKey ] ) ;
53+
54+ const encryptAndStore = useCallback ( async ( key :CryptoKey , v :T ) => {
55+ const cipher = await encryptData ( key , JSON . stringify ( v ) ) ;
56+ localStorage . setItem ( `${ storageKey } .data` , cipher ) ;
57+ } , [ storageKey ] ) ;
58+
59+ const deriveAndSetKey = useCallback ( async ( rawIdBase64 :string ) => {
60+ const buf = base64URLStringToBuffer ( rawIdBase64 ) ;
61+ const key = await deriveKey ( buf ) ;
62+ setKey ( key ) ;
63+ return key ;
64+ } , [ ] ) ;
65+
66+ /* ──────────────────────────────────────────────── */
67+ /* Auto‑login on mount */
68+ /* ──────────────────────────────────────────────── */
69+
70+ useEffect ( ( ) => {
71+ const auto = async ( ) => {
72+ const id = localStorage . getItem ( 'userIdentifier' ) ;
73+ const hasBlob = ! ! localStorage . getItem ( `${ storageKey } .data` ) ;
74+ if ( ! id || ! hasBlob ) return ;
75+
76+ setLoad ( true ) ;
77+ try {
78+ const assertion = await startAuthentication ( ) ;
79+ const key = await deriveAndSetKey ( assertion . rawId ) ;
80+ const v = await decryptFromStorage ( key ) ;
81+ setVals ( v ) ;
82+ } catch ( e ) { console . error ( '[SecureForm] auto‑login failed' , e ) ; }
83+ finally { setLoad ( false ) ; }
84+ } ;
85+ auto ( ) ;
86+ } , [ storageKey , decryptFromStorage , deriveAndSetKey ] ) ;
87+
88+ /* ──────────────────────────────────────────────── */
89+ /* Public API */
90+ /* ──────────────────────────────────────────────── */
91+
92+ const login = useCallback ( async ( ) => {
93+ setLoad ( true ) ; setErr ( null ) ;
94+ try {
95+ const assertion = await startAuthentication ( ) ;
96+ const key = await deriveAndSetKey ( assertion . rawId ) ;
97+ const v = await decryptFromStorage ( key ) ;
98+ setVals ( v ) ;
99+ } catch ( e :any ) { setErr ( e . message || 'Login failed' ) ; throw e ; }
100+ finally { setLoad ( false ) ; }
101+ } , [ decryptFromStorage , deriveAndSetKey ] ) ;
102+
103+ const logout = useCallback ( ( ) => {
104+ setKey ( null ) ;
105+ setVals ( null ) ;
106+ setErr ( null ) ;
107+ } , [ ] ) ;
108+
109+ const setValues = useCallback ( async ( v :T ) => {
110+ if ( ! encryptionKey ) throw new Error ( 'Not authenticated' ) ;
111+ await encryptAndStore ( encryptionKey , v ) ;
112+ setVals ( v ) ;
113+ } , [ encryptAndStore , encryptionKey ] ) ;
114+
115+ /* ──────────────────────────────────────────────── */
116+ /* Fallback submit handler */
117+ /* ──────────────────────────────────────────────── */
118+
119+ const submit = useCallback ( async ( formVals :T ) => {
120+ setLoad ( true ) ; setErr ( null ) ;
121+ try {
122+ let key = encryptionKey ;
123+ if ( ! key ) {
124+ // first‑time user? → register
125+ const cred = await startRegistration ( storageKey ) ;
126+ key = await deriveAndSetKey ( cred . rawId ) ;
127+ localStorage . setItem ( 'userIdentifier' , cred . rawId ) ;
128+ }
129+ await encryptAndStore ( key ! , formVals ) ;
130+ setVals ( formVals ) ;
131+ } catch ( e :any ) { setErr ( e . message || 'Failed to save' ) ; throw e ; }
132+ finally { setLoad ( false ) ; }
133+ } , [ encryptionKey , deriveAndSetKey , encryptAndStore ] ) ;
134+
135+ const ctxValue : SecureContext < T > = {
136+ isAuthenticated : ! ! encryptionKey ,
137+ encryptionKey,
138+ values,
139+ login,
140+ logout,
141+ setValues,
142+ } ;
143+
144+ /* ──────────────────────────────────────────────── */
145+ /* Render */
146+ /* ──────────────────────────────────────────────── */
147+
148+ const needFallback = ! encryptionKey || values === null ;
149+
150+ return (
151+ < Ctx . Provider value = { ctxValue } >
152+ { needFallback
153+ ? fallback ( { submit, error : error || undefined , isLoading } )
154+ : children ( ctxValue ) }
155+ </ Ctx . Provider >
156+ ) ;
157+ }
158+
159+ /* Hook for consumers */
160+ export const useSecureForm = < T , > ( ) => {
161+ const c = useContext ( Ctx ) ;
162+ if ( ! c ) throw new Error ( 'useSecureForm must be inside SecureFormProvider' ) ;
163+ return c as SecureContext < T > ;
164+ } ;
0 commit comments