send/app/keychain.js

220 lines
4.9 KiB
JavaScript
Raw Normal View History

2018-01-24 18:23:13 +00:00
import { arrayToB64, b64ToArray } from './utils';
2018-07-26 05:26:11 +00:00
import { decryptStream, encryptStream } from './ece.js';
2018-01-24 18:23:13 +00:00
const encoder = new TextEncoder();
const decoder = new TextDecoder();
2018-01-31 19:12:36 +00:00
export default class Keychain {
2018-01-24 18:23:13 +00:00
constructor(secretKeyB64, nonce, ivB64) {
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
if (ivB64) {
this.iv = b64ToArray(ivB64);
} else {
2018-07-06 22:49:50 +00:00
this.iv = crypto.getRandomValues(new Uint8Array(12));
2018-01-24 18:23:13 +00:00
}
if (secretKeyB64) {
this.rawSecret = b64ToArray(secretKeyB64);
} else {
2018-07-06 22:49:50 +00:00
this.rawSecret = crypto.getRandomValues(new Uint8Array(16));
2018-01-24 18:23:13 +00:00
}
2018-07-06 22:49:50 +00:00
this.secretKeyPromise = crypto.subtle.importKey(
2018-01-24 18:23:13 +00:00
'raw',
this.rawSecret,
'HKDF',
false,
['deriveKey']
);
this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
2018-07-06 22:49:50 +00:00
return crypto.subtle.deriveKey(
2018-01-24 18:23:13 +00:00
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('encryption'),
hash: 'SHA-256'
},
secretKey,
{
name: 'AES-GCM',
length: 128
},
false,
['encrypt', 'decrypt']
);
});
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
2018-07-06 22:49:50 +00:00
return crypto.subtle.deriveKey(
2018-01-24 18:23:13 +00:00
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('metadata'),
hash: 'SHA-256'
},
secretKey,
{
name: 'AES-GCM',
length: 128
},
false,
['encrypt', 'decrypt']
);
});
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
2018-07-06 22:49:50 +00:00
return crypto.subtle.deriveKey(
2018-01-24 18:23:13 +00:00
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('authentication'),
hash: 'SHA-256'
},
secretKey,
{
name: 'HMAC',
hash: { name: 'SHA-256' }
},
true,
['sign']
);
});
}
get nonce() {
return this._nonce;
}
set nonce(n) {
2018-01-30 20:15:09 +00:00
if (n && n !== this._nonce) {
this._nonce = n;
2018-01-24 18:23:13 +00:00
}
}
setIV(ivB64) {
this.iv = b64ToArray(ivB64);
}
setPassword(password, shareUrl) {
2018-07-06 22:49:50 +00:00
this.authKeyPromise = crypto.subtle
2018-01-24 18:23:13 +00:00
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
'deriveKey'
])
.then(passwordKey =>
2018-07-06 22:49:50 +00:00
crypto.subtle.deriveKey(
2018-01-24 18:23:13 +00:00
{
name: 'PBKDF2',
salt: encoder.encode(shareUrl),
iterations: 100,
hash: 'SHA-256'
},
passwordKey,
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
)
);
}
setAuthKey(authKeyB64) {
2018-07-06 22:49:50 +00:00
this.authKeyPromise = crypto.subtle.importKey(
2018-01-24 18:23:13 +00:00
'raw',
b64ToArray(authKeyB64),
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
);
}
async authKeyB64() {
const authKey = await this.authKeyPromise;
2018-07-06 22:49:50 +00:00
const rawAuth = await crypto.subtle.exportKey('raw', authKey);
2018-01-24 18:23:13 +00:00
return arrayToB64(new Uint8Array(rawAuth));
}
async authHeader() {
const authKey = await this.authKeyPromise;
2018-07-06 22:49:50 +00:00
const sig = await crypto.subtle.sign(
2018-01-24 18:23:13 +00:00
{
name: 'HMAC'
},
authKey,
b64ToArray(this.nonce)
);
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
}
async encryptFile(plaintext) {
const encryptKey = await this.encryptKeyPromise;
2018-07-06 22:49:50 +00:00
const ciphertext = await crypto.subtle.encrypt(
2018-01-24 18:23:13 +00:00
{
name: 'AES-GCM',
iv: this.iv,
tagLength: 128
},
encryptKey,
plaintext
);
return ciphertext;
}
async encryptMetadata(metadata) {
const metaKey = await this.metaKeyPromise;
2018-07-06 22:49:50 +00:00
const ciphertext = await crypto.subtle.encrypt(
2018-01-24 18:23:13 +00:00
{
name: 'AES-GCM',
iv: new Uint8Array(12),
tagLength: 128
},
metaKey,
encoder.encode(
JSON.stringify({
iv: arrayToB64(this.iv),
name: metadata.name,
size: metadata.size,
2018-07-26 05:26:11 +00:00
type: metadata.type || 'application/octet-stream',
manifest: metadata.manifest || {}
2018-01-24 18:23:13 +00:00
})
)
);
return ciphertext;
}
2018-07-26 05:26:11 +00:00
encryptStream(plainStream) {
return encryptStream(plainStream, this.rawSecret);
2018-06-21 00:05:33 +00:00
}
2018-07-05 19:40:49 +00:00
decryptStream(cryptotext) {
2018-07-26 05:26:11 +00:00
return decryptStream(cryptotext, this.rawSecret);
2018-06-21 00:05:33 +00:00
}
2018-01-24 18:23:13 +00:00
async decryptFile(ciphertext) {
const encryptKey = await this.encryptKeyPromise;
2018-07-06 22:49:50 +00:00
const plaintext = await crypto.subtle.decrypt(
2018-01-24 18:23:13 +00:00
{
name: 'AES-GCM',
iv: this.iv,
tagLength: 128
},
encryptKey,
ciphertext
);
return plaintext;
}
async decryptMetadata(ciphertext) {
const metaKey = await this.metaKeyPromise;
2018-07-06 22:49:50 +00:00
const plaintext = await crypto.subtle.decrypt(
2018-01-24 18:23:13 +00:00
{
name: 'AES-GCM',
iv: new Uint8Array(12),
tagLength: 128
},
metaKey,
ciphertext
);
return JSON.parse(decoder.decode(plaintext));
}
}