mirror of
https://github.com/bitwarden/help
synced 2025-12-06 00:03:30 +00:00
576 lines
21 KiB
HTML
576 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
|
|
<title>Bitwarden Crypto</title>
|
|
|
|
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
|
|
rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,300italic,400italic,600italic"
|
|
rel="stylesheet">
|
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
|
rel="stylesheet">
|
|
|
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
<!--[if lt IE 9]>
|
|
<script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
|
<script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
|
<![endif]-->
|
|
|
|
<style>
|
|
body {
|
|
padding: 50px 0;
|
|
}
|
|
|
|
|
|
h1, h2, h3, h4, h5, h6 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 32px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 24px;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 18px;
|
|
}
|
|
|
|
h4 {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container" id="app">
|
|
<h1>Key Derivation</h1>
|
|
|
|
<form>
|
|
<div class="row">
|
|
<div class="col-sm-4">
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input type="email" id="email" class="form-control" v-model="email">
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-group">
|
|
<label for="masterPassword">Master Password</label>
|
|
<input type="password" id="masterPassword" class="form-control" v-model="masterPassword">
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-group">
|
|
<label for="pbkdf2Iterations">Client PBKDF2 Iterations</label>
|
|
<input type="number" id="pbkdf2Iterations" class="form-control" v-model="pbkdf2Iterations">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<h2>Master Key</h2>
|
|
<pre>{{masterKey.b64}}</pre>
|
|
|
|
<h2>Master Password Hash</h2>
|
|
<pre>{{masterKeyHash.b64}}</pre>
|
|
|
|
<h2>Generated Symmetric Key</h2>
|
|
<pre>{{symKey.key.b64}}</pre>
|
|
<h3>Encryption Key</h3>
|
|
<pre>{{symKey.encKey.b64}}</pre>
|
|
<h3>MAC Key</h3>
|
|
<pre>{{symKey.macKey.b64}}</pre>
|
|
<h3>Protected Symmetric Key</h3>
|
|
<pre>{{protectedSymKey.string}}</pre>
|
|
|
|
<h2>Generated RSA Keypair</h2>
|
|
<h3>Public Key</h3>
|
|
<pre>{{publicKey.b64}}</pre>
|
|
<h3>Private Key</h3>
|
|
<pre>{{privateKey.b64}}</pre>
|
|
<h3>Protected Private Key</h3>
|
|
<pre>{{protectedPrivateKey.string}}</pre>
|
|
|
|
<button type="button" id="deriveKeys" class="btn btn-primary" v-on:click="generateKeys">
|
|
<i class="fa fa-refresh"></i> Regenerate Keys
|
|
</button>
|
|
|
|
<hr />
|
|
|
|
<h1>Encryption</h1>
|
|
|
|
<form>
|
|
<div class="form-group">
|
|
<label for="secret">Secret Value</label>
|
|
<textarea id="secret" class="form-control" v-model="secret"></textarea>
|
|
</div>
|
|
</form>
|
|
|
|
<h2>The "Cipher String"</h2>
|
|
<pre>{{protectedSecret.string}}</pre>
|
|
|
|
<h2>Decrypt</h2>
|
|
<textarea class="form-control" v-model="unprotectedSecret" readonly></textarea>
|
|
</div>
|
|
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
|
<script src="https://unpkg.com/vue"></script>
|
|
|
|
<script>
|
|
(function () {
|
|
// Constants/Enums
|
|
|
|
const encTypes = {
|
|
AesCbc256_B64: 0,
|
|
AesCbc128_HmacSha256_B64: 1,
|
|
AesCbc256_HmacSha256_B64: 2,
|
|
Rsa2048_OaepSha256_B64: 3,
|
|
Rsa2048_OaepSha1_B64: 4,
|
|
Rsa2048_OaepSha256_HmacSha256_B64: 5,
|
|
Rsa2048_OaepSha1_HmacSha256_B64: 6
|
|
};
|
|
|
|
// Classes
|
|
|
|
class Cipher {
|
|
constructor(encType, iv, ct, mac) {
|
|
if (!arguments.length) {
|
|
this.encType = null;
|
|
this.iv = null;
|
|
this.ct = null;
|
|
this.mac = null;
|
|
this.string = null;
|
|
return;
|
|
}
|
|
|
|
this.encType = encType;
|
|
this.iv = iv;
|
|
this.ct = ct;
|
|
this.string = encType + '.' + iv.b64 + '|' + ct.b64;
|
|
|
|
this.mac = null;
|
|
if (mac) {
|
|
this.mac = mac;
|
|
this.string += ('|' + mac.b64);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ByteData {
|
|
constructor(buf) {
|
|
if (!arguments.length) {
|
|
this.arr = null;
|
|
this.b64 = null;
|
|
return;
|
|
}
|
|
|
|
this.arr = new Uint8Array(buf);
|
|
this.b64 = toB64(buf);
|
|
}
|
|
}
|
|
|
|
class SymmetricCryptoKey {
|
|
constructor(buf) {
|
|
if (!arguments.length) {
|
|
this.key = new ByteData();
|
|
this.encKey = new ByteData();
|
|
this.macKey = new ByteData();
|
|
return;
|
|
}
|
|
|
|
this.key = new ByteData(buf);
|
|
|
|
// First half
|
|
const encKey = this.key.arr.slice(0, this.key.arr.length / 2).buffer;
|
|
this.encKey = new ByteData(encKey);
|
|
|
|
// Second half
|
|
const macKey = this.key.arr.slice(this.key.arr.length / 2).buffer;
|
|
this.macKey = new ByteData(macKey);
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
function fromUtf8(str) {
|
|
const strUtf8 = unescape(encodeURIComponent(str));
|
|
const bytes = new Uint8Array(strUtf8.length);
|
|
for (let i = 0; i < strUtf8.length; i++) {
|
|
bytes[i] = strUtf8.charCodeAt(i);
|
|
}
|
|
return bytes.buffer;
|
|
}
|
|
|
|
function toUtf8(buf) {
|
|
const bytes = new Uint8Array(buf);
|
|
const encodedString = String.fromCharCode.apply(null, bytes);
|
|
return decodeURIComponent(escape(encodedString));;
|
|
}
|
|
|
|
function toB64(buf) {
|
|
let binary = '';
|
|
const bytes = new Uint8Array(buf);
|
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return window.btoa(binary);
|
|
}
|
|
|
|
function hasValue(str) {
|
|
return str && str !== '';
|
|
}
|
|
|
|
// Crypto
|
|
|
|
function pbkdf2(password, salt, iterations, length) {
|
|
const importAlg = {
|
|
name: 'PBKDF2'
|
|
};
|
|
|
|
const deriveAlg = {
|
|
name: 'PBKDF2',
|
|
salt: salt,
|
|
iterations: iterations,
|
|
hash: { name: 'SHA-256' }
|
|
};
|
|
|
|
const aesOptions = {
|
|
name: 'AES-CBC',
|
|
length: length
|
|
};
|
|
|
|
return window.crypto.subtle.importKey('raw', password, importAlg, false, ['deriveKey'])
|
|
.then((importedKey) => {
|
|
return window.crypto.subtle.deriveKey(deriveAlg, importedKey, aesOptions, true, ['encrypt']);
|
|
}).then((derivedKey) => {
|
|
return window.crypto.subtle.exportKey('raw', derivedKey);
|
|
}).then((exportedKey) => {
|
|
return new ByteData(exportedKey);
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function aesEncrypt(data, encKey, macKey) {
|
|
const keyOptions = {
|
|
name: 'AES-CBC'
|
|
};
|
|
|
|
const encOptions = {
|
|
name: 'AES-CBC',
|
|
iv: new Uint8Array(16)
|
|
};
|
|
window.crypto.getRandomValues(encOptions.iv);
|
|
const ivData = new ByteData(encOptions.iv.buffer);
|
|
|
|
let ctData, macData;
|
|
return window.crypto.subtle.importKey('raw', encKey.arr.buffer, keyOptions, false, ['encrypt'])
|
|
.then((importedKey) => {
|
|
return window.crypto.subtle.encrypt(encOptions, importedKey, data);
|
|
}).then((encryptedBuffer) => {
|
|
ctData = new ByteData(encryptedBuffer);
|
|
if (!macKey) {
|
|
return null;
|
|
}
|
|
const dataForMac = buildDataForMac(ivData.arr, ctData.arr);
|
|
return computeMac(dataForMac.buffer, macKey.arr.buffer);
|
|
}).then((macBuffer) => {
|
|
let type = encTypes.AesCbc256_B64;
|
|
if (macBuffer) {
|
|
type = encTypes.AesCbc256_HmacSha256_B64;
|
|
macData = new ByteData(macBuffer);
|
|
}
|
|
return new Cipher(type, ivData, ctData, macData);
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function aesDecrypt(cipher, encKey, macKey) {
|
|
const keyOptions = {
|
|
name: 'AES-CBC'
|
|
};
|
|
|
|
const decOptions = {
|
|
name: 'AES-CBC',
|
|
iv: cipher.iv.arr.buffer
|
|
};
|
|
|
|
const checkMacPromise = new Promise((resolve) => {
|
|
if (cipher.encType == encTypes.AesCbc256_B64) {
|
|
resolve(false);
|
|
return;
|
|
}
|
|
if (!macKey) {
|
|
throw 'MAC key not provided.';
|
|
}
|
|
resolve(true);
|
|
});
|
|
|
|
return checkMacPromise
|
|
.then((checkMac) => {
|
|
if (!checkMac) {
|
|
return null;
|
|
}
|
|
const dataForMac = buildDataForMac(cipher.iv.arr, cipher.ct.arr);
|
|
return computeMac(dataForMac.buffer, macKey.arr.buffer)
|
|
})
|
|
.then((macBuffer) => {
|
|
if (!macBuffer) {
|
|
return true;
|
|
}
|
|
return macsEqual(cipher.mac.arr.buffer, macBuffer, macKey.arr.buffer);
|
|
}).then((macsMatch) => {
|
|
if (macsMatch === false) {
|
|
throw 'MAC check failed.';
|
|
}
|
|
return window.crypto.subtle.importKey('raw', encKey.arr.buffer, keyOptions, false, ['decrypt']);
|
|
}).then((importedKey) => {
|
|
return window.crypto.subtle.decrypt(decOptions, importedKey, cipher.ct.arr.buffer);
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function computeMac(data, key) {
|
|
const alg = {
|
|
name: 'HMAC',
|
|
hash: { name: 'SHA-256' }
|
|
};
|
|
|
|
return window.crypto.subtle.importKey('raw', key, alg, false, ['sign'])
|
|
.then((importedKey) => {
|
|
return window.crypto.subtle.sign(alg, importedKey, data);
|
|
});
|
|
}
|
|
|
|
function macsEqual(mac1Data, mac2Data, key) {
|
|
const alg = {
|
|
name: 'HMAC',
|
|
hash: { name: 'SHA-256' }
|
|
};
|
|
|
|
let mac1, importedMacKey;
|
|
return window.crypto.subtle.importKey('raw', key, alg, false, ['sign'])
|
|
.then((importedKey) => {
|
|
importedMacKey = importedKey;
|
|
return window.crypto.subtle.sign(alg, importedMacKey, mac1Data);
|
|
}).then((mac) => {
|
|
mac1 = mac;
|
|
return window.crypto.subtle.sign(alg, importedMacKey, mac2Data);
|
|
}).then((mac2) => {
|
|
if (mac1.byteLength !== mac2.byteLength) {
|
|
return false;
|
|
}
|
|
|
|
const arr1 = new Uint8Array(mac1);
|
|
const arr2 = new Uint8Array(mac2);
|
|
|
|
for (let i = 0; i < arr2.length; i++) {
|
|
if (arr1[i] !== arr2[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function buildDataForMac(ivArr, ctArr) {
|
|
const dataForMac = new Uint8Array(ivArr.length + ctArr.length);
|
|
dataForMac.set(ivArr, 0);
|
|
dataForMac.set(ctArr, ivArr.length);
|
|
return dataForMac;
|
|
}
|
|
|
|
function generateRsaKeypair() {
|
|
const rsaOptions = {
|
|
name: 'RSA-OAEP',
|
|
modulusLength: 2048,
|
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
|
|
hash: { name: 'SHA-1' }
|
|
};
|
|
|
|
let keypair, publicKey;
|
|
return window.crypto.subtle.generateKey(rsaOptions, true, ['encrypt', 'decrypt'])
|
|
.then((generatedKey) => {
|
|
keypair = generatedKey;
|
|
return window.crypto.subtle.exportKey('spki', keypair.publicKey);
|
|
}).then((exportedKey) => {
|
|
publicKey = new ByteData(exportedKey);
|
|
return window.crypto.subtle.exportKey('pkcs8', keypair.privateKey);
|
|
}).then((exportedKey) => {
|
|
return {
|
|
publicKey: publicKey,
|
|
privateKey: new ByteData(exportedKey)
|
|
};
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
// App
|
|
|
|
const vm = new Vue({
|
|
el: '#app',
|
|
data: {
|
|
email: '',
|
|
masterPassword: '',
|
|
pbkdf2Iterations: 5000,
|
|
|
|
masterKey: new ByteData(),
|
|
masterKeyHash: new ByteData(),
|
|
|
|
symKey: new SymmetricCryptoKey(),
|
|
protectedSymKey: new Cipher(),
|
|
unprotectedSymKey: new ByteData(),
|
|
|
|
publicKey: new ByteData(),
|
|
privateKey: new ByteData(),
|
|
protectedPrivateKey: new Cipher(),
|
|
unprotectedPrivateKey: new ByteData(),
|
|
|
|
secret: '',
|
|
protectedSecret: new Cipher(),
|
|
unprotectedSecret: ''
|
|
},
|
|
computed: {
|
|
masterPasswordBuffer() {
|
|
return this.masterPassword ? fromUtf8(this.masterPassword) : null;
|
|
},
|
|
emailBuffer() {
|
|
return this.email ? fromUtf8(this.email) : null;
|
|
},
|
|
secretBuffer() {
|
|
return this.secret ? fromUtf8(this.secret) : null;
|
|
}
|
|
},
|
|
watch: {
|
|
masterKey(newValue) {
|
|
const self = this;
|
|
|
|
if (!newValue || !newValue.arr || !self.masterPasswordBuffer) {
|
|
return new ByteData();
|
|
}
|
|
|
|
pbkdf2(newValue.arr.buffer, self.masterPasswordBuffer, 1, 256)
|
|
.then((masterKeyHash) => {
|
|
self.masterKeyHash = masterKeyHash;
|
|
});
|
|
}
|
|
},
|
|
methods: {
|
|
generateKeys() {
|
|
const self = this;
|
|
|
|
const symKey = new Uint8Array(512 / 8);
|
|
window.crypto.getRandomValues(symKey);
|
|
self.symKey = new SymmetricCryptoKey(symKey);
|
|
|
|
generateRsaKeypair().then((keypair) => {
|
|
self.publicKey = keypair.publicKey;
|
|
self.privateKey = keypair.privateKey;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
vm.$watch(() => {
|
|
return {
|
|
masterPassword: vm.masterPasswordBuffer,
|
|
email: vm.emailBuffer,
|
|
iterations: vm.pbkdf2Iterations
|
|
};
|
|
}, (newVal, oldVal) => {
|
|
if (!newVal.masterPassword || !newVal.email || !newVal.iterations || newVal.iterations < 1) {
|
|
vm.masterKey = new ByteData();
|
|
return;
|
|
}
|
|
|
|
pbkdf2(newVal.masterPassword, newVal.email, newVal.iterations, 256)
|
|
.then((masterKey) => {
|
|
vm.masterKey = masterKey;
|
|
});
|
|
});
|
|
|
|
vm.$watch(() => {
|
|
return {
|
|
symKey: vm.symKey,
|
|
secret: vm.secretBuffer
|
|
};
|
|
}, (newVal, oldVal) => {
|
|
if (!newVal.symKey || !newVal.secret) {
|
|
vm.protectedSecret = new Cipher();
|
|
vm.unprotectedSecret = '';
|
|
return;
|
|
}
|
|
|
|
aesEncrypt(newVal.secret, newVal.symKey.encKey, newVal.symKey.macKey)
|
|
.then((cipher) => {
|
|
vm.protectedSecret = cipher;
|
|
return aesDecrypt(vm.protectedSecret, newVal.symKey.encKey, newVal.symKey.macKey);
|
|
}).then((secret) => {
|
|
vm.unprotectedSecret = toUtf8(secret);
|
|
});
|
|
});
|
|
|
|
vm.$watch(() => {
|
|
return {
|
|
masterKey: vm.masterKey,
|
|
symKey: vm.symKey
|
|
};
|
|
}, (newVal, oldVal) => {
|
|
if (!newVal.masterKey || !newVal.masterKey.arr || !newVal.symKey || !newVal.symKey.key) {
|
|
vm.protectedSymKey = new Cipher();
|
|
return;
|
|
}
|
|
|
|
aesEncrypt(newVal.symKey.key.arr, newVal.masterKey, null)
|
|
.then((cipher) => {
|
|
vm.protectedSymKey = cipher;
|
|
return aesDecrypt(vm.protectedSymKey, newVal.masterKey, null);
|
|
}).then((unprotectedSymKey) => {
|
|
vm.unprotectedSymKey = new ByteData(unprotectedSymKey);
|
|
});
|
|
});
|
|
|
|
vm.$watch(() => {
|
|
return {
|
|
symKey: vm.symKey,
|
|
privateKey: vm.privateKey
|
|
};
|
|
}, (newVal, oldVal) => {
|
|
if (!newVal.symKey || !newVal.symKey.key || !newVal.privateKey || !newVal.privateKey.arr) {
|
|
vm.protectedPrivateKey = new Cipher();
|
|
return;
|
|
}
|
|
|
|
aesEncrypt(newVal.privateKey.arr, newVal.symKey.encKey, newVal.symKey.macKey)
|
|
.then((cipher) => {
|
|
vm.protectedPrivateKey = cipher;
|
|
return aesDecrypt(vm.protectedPrivateKey, newVal.symKey.encKey, newVal.symKey.macKey);
|
|
}).then((unprotectedPrivateKey) => {
|
|
vm.unprotectedPrivateKey = new ByteData(unprotectedPrivateKey);
|
|
});
|
|
});
|
|
|
|
// Set default values
|
|
vm.email = 'user@example.com';
|
|
vm.masterPassword = 'password123';
|
|
vm.secret = 'This is a secret.';
|
|
|
|
// Generate initial set of keys
|
|
vm.generateKeys();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|