1+ <!--
2+ Copyright 2025 cbe
3+
4+ Licensed under the Apache License, Version 2.0 (the "License");
5+ you may not use this file except in compliance with the License.
6+ You may obtain a copy of the License at
7+
8+ https://www.apache.org/licenses/LICENSE-2.0
9+
10+ Unless required by applicable law or agreed to in writing, software
11+ distributed under the License is distributed on an "AS IS" BASIS,
12+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ See the License for the specific language governing permissions and
14+ limitations under the License.
15+ -->
16+
17+ <template >
18+ <div >
19+ <div class =" fixed inset-0 z-50 overflow-y-auto" >
20+ <div class =" flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0" >
21+ <div class =" fixed inset-0 transition-opacity" aria-hidden =" true" >
22+ <div class =" absolute inset-0 bg-gray-500 opacity-75" />
23+ </div >
24+
25+ <span class =" hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden =" true" >​ ; </span >
26+
27+ <div
28+ class =" inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" >
29+ <div class =" bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4" >
30+ <div class =" sm:flex sm:items-start" >
31+ <div
32+ class =" mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" >
33+ <svg
34+ class =" h-6 w-6 text-blue-600" xmlns =" http://www.w3.org/2000/svg" fill =" none" viewBox =" 0 0 24 24"
35+ stroke =" currentColor" aria-hidden =" true" >
36+ <path
37+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
38+ d =" M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
39+ </svg >
40+ </div >
41+ <div class =" mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left" >
42+ <h3 class =" text-lg leading-6 font-medium text-gray-900" >
43+ Create Crypto Account
44+ </h3 >
45+ <p >
46+ Choose a password to protect your account
47+ </p >
48+ <div class =" mt-2 relative" >
49+ <input
50+ v-model =" password" :type =" showPassword ? 'text' : 'password'" placeholder =" Password"
51+ class =" w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" >
52+ <button
53+ type =" button" class =" absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500"
54+ @click =" showPassword = !showPassword" >
55+ <svg
56+ v-if =" showPassword" class =" h-5 w-5" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24"
57+ xmlns =" http://www.w3.org/2000/svg" >
58+ <path
59+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
60+ d =" M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
61+ <path
62+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
63+ d =" M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
64+ </svg >
65+ <svg
66+ v-else class =" h-5 w-5" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24"
67+ xmlns =" http://www.w3.org/2000/svg" >
68+ <path
69+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
70+ d =" M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
71+ </svg >
72+ </button >
73+ </div >
74+ <div class =" mt-2 relative" >
75+ <input
76+ v-model =" confirmPassword" :type =" showConfirmPassword ? 'text' : 'password'"
77+ placeholder =" Confirm Password"
78+ class =" w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" >
79+ <button
80+ type =" button" class =" absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500"
81+ @click =" showConfirmPassword = !showConfirmPassword" >
82+ <svg
83+ v-if =" showConfirmPassword" class =" h-5 w-5" fill =" none" stroke =" currentColor"
84+ viewBox =" 0 0 24 24" xmlns =" http://www.w3.org/2000/svg" >
85+ <path
86+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
87+ d =" M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
88+ <path
89+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
90+ d =" M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
91+ </svg >
92+ <svg
93+ v-else class =" h-5 w-5" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24"
94+ xmlns =" http://www.w3.org/2000/svg" >
95+ <path
96+ stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2"
97+ d =" M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
98+ </svg >
99+ </button >
100+ </div >
101+ <div v-if =" showMatchError" class =" text-red-500 text-sm mt-2" >Passwords do not match!</div >
102+ </div >
103+ </div >
104+ </div >
105+ <div class =" bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse" >
106+ <button
107+ type =" button"
108+ class =" w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
109+ @click =" checkPassword" >
110+ Confirm
111+ </button >
112+ <button
113+ type =" button"
114+ class =" mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
115+ @click =" $emit('cancel')" >
116+ Cancel
117+ </button >
118+ </div >
119+ </div >
120+ </div >
121+ </div >
122+ </div >
123+ </template >
124+
125+ <script setup lang="ts">
126+ import { toHex , type HDAccount , type Hex } from " viem" ;
127+ import { english , generateMnemonic , mnemonicToAccount } from " viem/accounts" ;
128+ import { deployAccount , fetchAccount } from " zksync-sso/client/ecdsa" ;
129+ import { getDeployerClient } from " ../common/CryptoDeployer" ;
130+
131+ const { appMeta, contracts, deployerKey } = useAppMeta ();
132+
133+ const password = ref (" " );
134+ const showPassword = ref (false );
135+ const confirmPassword = ref (" " );
136+ const showConfirmPassword = ref (false );
137+ const showMatchError = ref (false );
138+ const account = ref <HDAccount >();
139+ const mnemonic = ref <string >();
140+
141+ // create account with EOA owner
142+ onMounted (() => {
143+ mnemonic .value = generateMnemonic (english );
144+ account .value = mnemonicToAccount (mnemonic .value );
145+ });
146+
147+ async function encryptMessage(cryptoSecretKey : string , password : string , iv : Uint8Array , salt : Uint8Array ) {
148+ // hashed pin/password
149+ const keyMaterial = await window .crypto .subtle .importKey (
150+ " raw" ,
151+ new TextEncoder ().encode (password ),
152+ " PBKDF2" ,
153+ false ,
154+ [" deriveBits" , " deriveKey" ],
155+ );
156+ const keyEncryptionKey = await window .crypto .subtle .deriveKey (
157+ {
158+ name: " PBKDF2" ,
159+ salt ,
160+ iterations: 100000 ,
161+ hash: " SHA-256" ,
162+ },
163+ keyMaterial ,
164+ { name: " AES-GCM" , length: 256 },
165+ true ,
166+ [" encrypt" , " decrypt" ],
167+ );
168+ const encryptedCryptoSecret = await window .crypto .subtle .encrypt (
169+ {
170+ name: " AES-GCM" ,
171+ iv: iv
172+ },
173+ keyEncryptionKey ,
174+ new TextEncoder ().encode (cryptoSecretKey ),
175+ );
176+
177+ return { encryptedCryptoSecret , keyEncryptionKey };
178+ }
179+
180+ async function decryptMessage(encryptedKey : ArrayBuffer , key : CryptoKey , iv : Uint8Array ) {
181+ const decryptedBytes = await window .crypto .subtle .decrypt ({ name: " AES-GCM" , iv }, key , encryptedKey );
182+ return new TextDecoder ().decode (decryptedBytes );
183+ }
184+
185+ async function checkPassword() {
186+ if (! mnemonic .value ) {
187+ console .warn (" Private key not set" );
188+ return ;
189+ }
190+ if (! account .value ) {
191+ console .warn (" account not ready" );
192+ return ;
193+ }
194+ if (password .value !== confirmPassword .value ) {
195+ console .warn (" no password match" );
196+ showMatchError .value = true ;
197+ return ;
198+ }
199+ // TODO: get iv and salt from server!
200+ const iv = new Uint8Array (12 );
201+ const salt = new Uint8Array (16 );
202+ if (! password .value ) {
203+ console .warn (" Pin not set" );
204+ return ;
205+ }
206+ const encrypted = await encryptMessage (mnemonic .value , password .value , iv , salt );
207+ if (! encrypted ) {
208+ console .error (" Encryption failed" , mnemonic .value );
209+ return ;
210+ }
211+ const testDecrypted = await decryptMessage (
212+ encrypted .encryptedCryptoSecret , encrypted .keyEncryptionKey , iv );
213+ if (mnemonic .value != testDecrypted ) {
214+ console .error (" Decryption failed" , mnemonic .value , testDecrypted );
215+ return ;
216+ }
217+ console .log (" Decryption success" , encrypted );
218+
219+ const address = await deployAddress (account .value );
220+ const pk = account .value .getHdKey ().privateKey ;
221+ if (! pk ) {
222+ console .warn (" account has no private key!" );
223+ return ;
224+ }
225+
226+ appMeta .value = {
227+ ... appMeta .value ,
228+ cryptoAccountAddress: address ,
229+ privateKey: toHex (pk ),
230+ };
231+
232+ downloadArrayBuffer (" encrypted-private-key.txt" , encrypted .encryptedCryptoSecret );
233+ navigateTo (" /crypto-account" );
234+ confirm ();
235+ }
236+
237+ async function deployAddress(accountAddress : HDAccount ) {
238+ const deployerClient = await getDeployerClient (deployerKey as Hex );
239+ try {
240+ const fetchedAccount = await fetchAccount (deployerClient , {
241+ contracts ,
242+ prefix: " bank-demo" ,
243+ owner: accountAddress .address ,
244+ });
245+ return fetchedAccount .address ;
246+ } catch (err ) {
247+ console .info (" account does not exist, deploy!" , err );
248+ const deployedAccount = await deployAccount (deployerClient , {
249+ contracts ,
250+ prefix: " bank-demo" ,
251+ owner: accountAddress .address ,
252+ });
253+ return deployedAccount .address ;
254+ }
255+ }
256+
257+ function downloadArrayBuffer(filename : string , buffer : ArrayBuffer , mimeType : string = " application/octet-stream" ) {
258+ const blob = new Blob ([buffer ], { type: mimeType });
259+ const url = window .URL .createObjectURL (blob );
260+
261+ const a = document .createElement (" a" );
262+ a .href = url ;
263+ a .download = filename ;
264+
265+ a .style .display = " none" ;
266+ document .body .appendChild (a );
267+
268+ a .click ();
269+
270+ window .URL .revokeObjectURL (url );
271+ document .body .removeChild (a );
272+ }
273+
274+ defineEmits ([" confirm" , " cancel" ]);
275+ </script >
0 commit comments