Skip to content
This repository was archived by the owner on May 22, 2021. It is now read-only.

Commit bc24a06

Browse files
committed
Add optional password to the download url
1 parent 837747f commit bc24a06

28 files changed

Lines changed: 805 additions & 241 deletions

app/fileManager.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export default function(state, emitter) {
153153
state.storage.totalUploads += 1;
154154
emitter.emit('pushState', `/share/${info.id}`);
155155
} catch (err) {
156+
console.error(err);
156157
state.transfer = null;
157158
if (err.message === '0') {
158159
//cancelled. do nothing
@@ -161,23 +162,51 @@ export default function(state, emitter) {
161162
}
162163
state.raven.captureException(err);
163164
metrics.stoppedUpload({ size, type, err });
164-
emitter.emit('replaceState', '/error');
165+
emitter.emit('pushState', '/error');
165166
}
166167
});
167168

168-
emitter.on('download', async file => {
169-
const size = file.size;
169+
emitter.on('password', async ({ password, file }) => {
170+
try {
171+
await FileSender.setPassword(password, file);
172+
metrics.addedPassword({ size: file.size });
173+
file.password = password;
174+
state.storage.writeFiles();
175+
} catch (e) {
176+
console.error(e);
177+
}
178+
render();
179+
});
180+
181+
emitter.on('preview', async () => {
182+
const file = state.fileInfo;
170183
const url = `/api/download/${file.id}`;
171-
const receiver = new FileReceiver(url, file.key);
184+
const receiver = new FileReceiver(url, file);
172185
receiver.on('progress', updateProgress);
173186
receiver.on('decrypting', render);
174187
state.transfer = receiver;
175-
const links = openLinksInNewTab();
188+
try {
189+
await receiver.getMetadata(file.nonce);
190+
} catch (e) {
191+
if (e.message === '401') {
192+
file.password = null;
193+
if (!file.pwd) {
194+
return emitter.emit('pushState', '/404');
195+
}
196+
}
197+
}
176198
render();
199+
});
200+
201+
emitter.on('download', async file => {
202+
state.transfer.on('progress', render);
203+
state.transfer.on('decrypting', render);
204+
const links = openLinksInNewTab();
205+
const size = file.size;
177206
try {
178207
const start = Date.now();
179208
metrics.startedDownload({ size: file.size, ttl: file.ttl });
180-
const f = await receiver.download();
209+
const f = await state.transfer.download(file.nonce);
181210
const time = Date.now() - start;
182211
const speed = size / (time / 1000);
183212
await delay(1000);
@@ -187,13 +216,14 @@ export default function(state, emitter) {
187216
metrics.completedDownload({ size, time, speed });
188217
emitter.emit('pushState', '/completed');
189218
} catch (err) {
219+
console.error(err);
190220
// TODO cancelled download
191221
const location = err.message === 'notfound' ? '/404' : '/error';
192222
if (location === '/error') {
193223
state.raven.captureException(err);
194224
metrics.stoppedDownload({ size, err });
195225
}
196-
emitter.emit('replaceState', location);
226+
emitter.emit('pushState', location);
197227
} finally {
198228
state.transfer = null;
199229
openLinksInNewTab(links, false);

app/fileReceiver.js

Lines changed: 196 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,104 @@
11
import Nanobus from 'nanobus';
2-
import { hexToArray, bytes } from './utils';
2+
import { arrayToB64, b64ToArray, bytes } from './utils';
33

44
export 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

Comments
 (0)