11import Nanobus from 'nanobus' ;
2- import { hexToArray , bytes } from './utils' ;
2+ import { arrayToB64 , b64ToArray , bytes } from './utils' ;
33
44export default class FileReceiver extends Nanobus {
5- constructor ( url , k ) {
5+ constructor ( url , file ) {
66 super ( 'FileReceiver' ) ;
7- this . key = window . crypto . subtle . importKey (
8- 'jwk' ,
9- {
10- k,
11- kty : 'oct' ,
12- alg : 'A128GCM' ,
13- ext : true
14- } ,
15- {
16- name : 'AES-GCM'
17- } ,
7+ this . secretKeyPromise = window . crypto . subtle . importKey (
8+ 'raw' ,
9+ b64ToArray ( file . key ) ,
10+ 'HKDF' ,
1811 false ,
19- [ 'decrypt ' ]
12+ [ 'deriveKey ' ]
2013 ) ;
14+ this . encryptKeyPromise = this . secretKeyPromise . then ( sk => {
15+ const encoder = new TextEncoder ( ) ;
16+ return window . crypto . subtle . deriveKey (
17+ {
18+ name : 'HKDF' ,
19+ salt : new Uint8Array ( ) ,
20+ info : encoder . encode ( 'encryption' ) ,
21+ hash : 'SHA-256'
22+ } ,
23+ sk ,
24+ {
25+ name : 'AES-GCM' ,
26+ length : 128
27+ } ,
28+ false ,
29+ [ 'decrypt' ]
30+ ) ;
31+ } ) ;
32+ if ( file . pwd ) {
33+ const encoder = new TextEncoder ( ) ;
34+ console . log ( file . password + file . url ) ;
35+ this . authKeyPromise = window . crypto . subtle
36+ . importKey (
37+ 'raw' ,
38+ encoder . encode ( file . password ) ,
39+ { name : 'PBKDF2' } ,
40+ false ,
41+ [ 'deriveKey' ]
42+ )
43+ . then ( pwdKey =>
44+ window . crypto . subtle . deriveKey (
45+ {
46+ name : 'PBKDF2' ,
47+ salt : encoder . encode ( file . url ) ,
48+ iterations : 100 ,
49+ hash : 'SHA-256'
50+ } ,
51+ pwdKey ,
52+ {
53+ name : 'HMAC' ,
54+ hash : 'SHA-256'
55+ } ,
56+ true ,
57+ [ 'sign' ]
58+ )
59+ ) ;
60+ } else {
61+ this . authKeyPromise = this . secretKeyPromise . then ( sk => {
62+ const encoder = new TextEncoder ( ) ;
63+ return window . crypto . subtle . deriveKey (
64+ {
65+ name : 'HKDF' ,
66+ salt : new Uint8Array ( ) ,
67+ info : encoder . encode ( 'authentication' ) ,
68+ hash : 'SHA-256'
69+ } ,
70+ sk ,
71+ {
72+ name : 'HMAC' ,
73+ hash : { name : 'SHA-256' }
74+ } ,
75+ false ,
76+ [ 'sign' ]
77+ ) ;
78+ } ) ;
79+ }
80+ this . metaKeyPromise = this . secretKeyPromise . then ( sk => {
81+ const encoder = new TextEncoder ( ) ;
82+ return window . crypto . subtle . deriveKey (
83+ {
84+ name : 'HKDF' ,
85+ salt : new Uint8Array ( ) ,
86+ info : encoder . encode ( 'metadata' ) ,
87+ hash : 'SHA-256'
88+ } ,
89+ sk ,
90+ {
91+ name : 'AES-GCM' ,
92+ length : 128
93+ } ,
94+ false ,
95+ [ 'decrypt' ]
96+ ) ;
97+ } ) ;
98+ this . file = file ;
2199 this . url = url ;
22100 this . msg = 'fileSizeProgress' ;
101+ this . state = 'initialized' ;
23102 this . progress = [ 0 , 1 ] ;
24103 }
25104
@@ -38,7 +117,65 @@ export default class FileReceiver extends Nanobus {
38117 // TODO
39118 }
40119
41- downloadFile ( ) {
120+ fetchMetadata ( sig ) {
121+ return new Promise ( ( resolve , reject ) => {
122+ const xhr = new XMLHttpRequest ( ) ;
123+ xhr . onreadystatechange = ( ) => {
124+ if ( xhr . readyState === XMLHttpRequest . DONE ) {
125+ const nonce = xhr . getResponseHeader ( 'WWW-Authenticate' ) . split ( ' ' ) [ 1 ] ;
126+ this . file . nonce = nonce ;
127+ if ( xhr . status === 200 ) {
128+ return resolve ( xhr . response ) ;
129+ }
130+ reject ( new Error ( xhr . status ) ) ;
131+ }
132+ } ;
133+ xhr . onerror = ( ) => reject ( new Error ( 0 ) ) ;
134+ xhr . ontimeout = ( ) => reject ( new Error ( 0 ) ) ;
135+ xhr . open ( 'get' , `/api/metadata/${ this . file . id } ` ) ;
136+ xhr . setRequestHeader ( 'Authorization' , `send-v1 ${ arrayToB64 ( sig ) } ` ) ;
137+ xhr . responseType = 'json' ;
138+ xhr . timeout = 2000 ;
139+ xhr . send ( ) ;
140+ } ) ;
141+ }
142+
143+ async getMetadata ( nonce ) {
144+ try {
145+ const authKey = await this . authKeyPromise ;
146+ const sig = await window . crypto . subtle . sign (
147+ {
148+ name : 'HMAC'
149+ } ,
150+ authKey ,
151+ b64ToArray ( nonce )
152+ ) ;
153+ const data = await this . fetchMetadata ( new Uint8Array ( sig ) ) ;
154+ const metaKey = await this . metaKeyPromise ;
155+ const json = await window . crypto . subtle . decrypt (
156+ {
157+ name : 'AES-GCM' ,
158+ iv : new Uint8Array ( 12 ) ,
159+ tagLength : 128
160+ } ,
161+ metaKey ,
162+ b64ToArray ( data . metadata )
163+ ) ;
164+ const decoder = new TextDecoder ( ) ;
165+ const meta = JSON . parse ( decoder . decode ( json ) ) ;
166+ this . file . name = meta . name ;
167+ this . file . type = meta . type ;
168+ this . file . iv = meta . iv ;
169+ this . file . size = data . size ;
170+ this . file . ttl = data . ttl ;
171+ this . state = 'ready' ;
172+ } catch ( e ) {
173+ this . state = 'invalid' ;
174+ throw e ;
175+ }
176+ }
177+
178+ downloadFile ( sig ) {
42179 return new Promise ( ( resolve , reject ) => {
43180 const xhr = new XMLHttpRequest ( ) ;
44181
@@ -49,52 +186,67 @@ export default class FileReceiver extends Nanobus {
49186 }
50187 } ;
51188
52- xhr . onload = function ( event ) {
189+ xhr . onload = event => {
53190 if ( xhr . status === 404 ) {
54191 reject ( new Error ( 'notfound' ) ) ;
55192 return ;
56193 }
57194
58- const blob = new Blob ( [ this . response ] ) ;
59- const meta = JSON . parse ( xhr . getResponseHeader ( 'X-File-Metadata' ) ) ;
195+ if ( xhr . status !== 200 ) {
196+ return reject ( new Error ( xhr . status ) ) ;
197+ }
198+
199+ const blob = new Blob ( [ xhr . response ] ) ;
60200 const fileReader = new FileReader ( ) ;
61201 fileReader . onload = function ( ) {
62- resolve ( {
63- data : this . result ,
64- name : meta . filename ,
65- type : meta . mimeType ,
66- iv : meta . id
67- } ) ;
202+ resolve ( this . result ) ;
68203 } ;
69204
70205 fileReader . readAsArrayBuffer ( blob ) ;
71206 } ;
72207
73208 xhr . open ( 'get' , this . url ) ;
209+ xhr . setRequestHeader ( 'Authorization' , `send-v1 ${ arrayToB64 ( sig ) } ` ) ;
74210 xhr . responseType = 'blob' ;
75211 xhr . send ( ) ;
76212 } ) ;
77213 }
78214
79- async download ( ) {
80- const key = await this . key ;
81- const file = await this . downloadFile ( ) ;
82- this . msg = 'decryptingFile' ;
83- this . emit ( 'decrypting' ) ;
84- const plaintext = await window . crypto . subtle . decrypt (
85- {
86- name : 'AES-GCM' ,
87- iv : hexToArray ( file . iv ) ,
88- tagLength : 128
89- } ,
90- key ,
91- file . data
92- ) ;
93- this . msg = 'downloadFinish' ;
94- return {
95- plaintext,
96- name : decodeURIComponent ( file . name ) ,
97- type : file . type
98- } ;
215+ async download ( nonce ) {
216+ this . state = 'downloading' ;
217+ this . emit ( 'progress' , this . progress ) ;
218+ try {
219+ const encryptKey = await this . encryptKeyPromise ;
220+ const authKey = await this . authKeyPromise ;
221+ const sig = await window . crypto . subtle . sign (
222+ {
223+ name : 'HMAC'
224+ } ,
225+ authKey ,
226+ b64ToArray ( nonce )
227+ ) ;
228+ const ciphertext = await this . downloadFile ( new Uint8Array ( sig ) ) ;
229+ this . msg = 'decryptingFile' ;
230+ this . emit ( 'decrypting' ) ;
231+ const plaintext = await window . crypto . subtle . decrypt (
232+ {
233+ name : 'AES-GCM' ,
234+ iv : b64ToArray ( this . file . iv ) ,
235+ tagLength : 128
236+ } ,
237+ encryptKey ,
238+ ciphertext
239+ ) ;
240+ this . msg = 'downloadFinish' ;
241+ this . state = 'complete' ;
242+ return {
243+ plaintext,
244+ name : decodeURIComponent ( this . file . name ) ,
245+ type : this . file . type
246+ } ;
247+ } catch ( e ) {
248+ this . state = 'invalid' ;
249+ throw e ;
250+ }
99251 }
100252}
0 commit comments