mirror of
https://github.com/bitwarden/help
synced 2025-12-06 00:03:30 +00:00
437 lines
16 KiB
HTML
437 lines
16 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]-->
|
|
</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>User Key</h2>
|
|
<p>{{userKey.b64}}</p>
|
|
|
|
<h2>Master Password Hash</h2>
|
|
<p>{{userKeyHash.b64}}</p>
|
|
|
|
<h2>Generated Symmetric Key Material</h2>
|
|
<p>{{key.key.b64}}</p>
|
|
<h3>Encryption Key</h3>
|
|
<p>{{key.encKey.b64}}</p>
|
|
<h3>MAC Key</h3>
|
|
<p>{{key.macKey.b64}}</p>
|
|
|
|
<h2>Generated RSA Keypair</h2>
|
|
<h3>Public Key</h3>
|
|
<p></p>
|
|
<h3>Private Key</h3>
|
|
<p></p>
|
|
|
|
<button type="button" id="deriveKeys" class="btn btn-primary" v-on:click="generateKeys">
|
|
Regenerate Keys
|
|
</button>
|
|
|
|
<hr />
|
|
|
|
<h1>Encryption</h1>
|
|
|
|
<form>
|
|
<div class="form-group">
|
|
<label for="plaintext">Plaintext Value</label>
|
|
<input type="text" id="plaintext" class="form-control" v-model="plaintext">
|
|
</div>
|
|
</form>
|
|
|
|
<hr />
|
|
|
|
<h2>The "Cipher String"</h2>
|
|
<p>{{cipher.string}}</p>
|
|
|
|
<h2>Decrypt</h2>
|
|
<p>{{decPlaintext}}</p>
|
|
</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
|
|
|
|
var encType = {
|
|
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
|
|
|
|
var Cipher = function (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);
|
|
}
|
|
}
|
|
|
|
var ByteData = function (buf) {
|
|
if (!arguments.length) {
|
|
this.arr = null;
|
|
this.b64 = null;
|
|
return;
|
|
}
|
|
|
|
this.arr = new Uint8Array(buf);
|
|
this.b64 = toB64(buf);
|
|
}
|
|
|
|
var SymmetricCryptoKey = function (buf) {
|
|
if (!arguments.length) {
|
|
this.key = new ByteData();
|
|
this.encKey = new ByteData();
|
|
this.macKey = new ByteData();
|
|
return;
|
|
}
|
|
|
|
this.key = new ByteData(buf);
|
|
|
|
// First half
|
|
var encKey = this.key.arr.slice(0, this.key.arr.length / 2).buffer;
|
|
this.encKey = new ByteData(encKey);
|
|
|
|
// Second half
|
|
var macKey = this.key.arr.slice(this.key.arr.length / 2).buffer;
|
|
this.macKey = new ByteData(macKey);
|
|
}
|
|
|
|
// Helpers
|
|
|
|
function fromUtf8(str) {
|
|
var strUtf8 = unescape(encodeURIComponent(str)),
|
|
bytes = new Uint8Array(strUtf8.length);
|
|
for (var i = 0; i < strUtf8.length; i++) {
|
|
bytes[i] = strUtf8.charCodeAt(i);
|
|
}
|
|
return bytes.buffer;
|
|
}
|
|
|
|
function toUtf8(buf) {
|
|
var bytes = new Uint8Array(buf),
|
|
encodedString = String.fromCharCode.apply(null, bytes),
|
|
decodedString = decodeURIComponent(escape(encodedString));
|
|
return decodedString;
|
|
}
|
|
|
|
function fromB64(str) {
|
|
var binary_string = window.atob(str),
|
|
len = binary_string.length,
|
|
bytes = new Uint8Array(len);
|
|
for (var i = 0; i < len; i++) {
|
|
bytes[i] = binary_string.charCodeAt(i);
|
|
}
|
|
return bytes.buffer;
|
|
}
|
|
|
|
function toB64(buf) {
|
|
var binary = '',
|
|
bytes = new Uint8Array(buf);
|
|
for (var 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) {
|
|
var importAlg = {
|
|
name: 'PBKDF2'
|
|
};
|
|
|
|
var deriveAlg = {
|
|
name: 'PBKDF2',
|
|
salt: salt,
|
|
iterations: iterations,
|
|
hash: { name: 'SHA-256' }
|
|
};
|
|
|
|
var aesOptions = {
|
|
name: 'AES-CBC',
|
|
length: 256
|
|
};
|
|
|
|
return window.crypto.subtle.importKey('raw', password, importAlg, false, ['deriveKey'])
|
|
.then(function (importedKey) {
|
|
return window.crypto.subtle.deriveKey(deriveAlg, importedKey, aesOptions, true, ['encrypt']);
|
|
}).then(function (derivedKey) {
|
|
return window.crypto.subtle.exportKey('raw', derivedKey);
|
|
}).then(function (exportedKey) {
|
|
return new ByteData(exportedKey);
|
|
}).catch(function (err) {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function aesEncrypt(data, key) {
|
|
var keyOptions = {
|
|
name: 'AES-CBC'
|
|
};
|
|
|
|
var encOptions = {
|
|
name: 'AES-CBC',
|
|
iv: new Uint8Array(16)
|
|
};
|
|
window.crypto.getRandomValues(encOptions.iv);
|
|
|
|
var ivData, ctData, macData;
|
|
return window.crypto.subtle.importKey('raw', key.encKey.arr.buffer, keyOptions, false, ['encrypt'])
|
|
.then(function (importedKey) {
|
|
return window.crypto.subtle.encrypt(encOptions, importedKey, data);
|
|
}).then(function (encryptedBuffer) {
|
|
ivData = new ByteData(encOptions.iv.buffer);
|
|
ctData = new ByteData(encryptedBuffer);
|
|
var dataForMac = buildDataForMac(ivData.arr, ctData.arr);
|
|
return computeMac(dataForMac.buffer, key.macKey.arr.buffer);
|
|
}).then(function (macBuffer) {
|
|
macData = new ByteData(macBuffer);
|
|
return new Cipher(encType.AesCbc256_HmacSha256_B64, ivData, ctData, macData);
|
|
}).catch(function (err) {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function aesDecrypt(cipher, key) {
|
|
var keyOptions = {
|
|
name: 'AES-CBC'
|
|
};
|
|
|
|
var decOptions = {
|
|
name: 'AES-CBC',
|
|
iv: cipher.iv.arr.buffer
|
|
};
|
|
|
|
var dataForMac = buildDataForMac(cipher.iv.arr, cipher.ct.arr);
|
|
return computeMac(dataForMac.buffer, key.macKey.arr.buffer)
|
|
.then(function (macBuffer) {
|
|
return macsEqual(cipher.mac.arr.buffer, macBuffer, key.macKey.arr.buffer);
|
|
}).then(function (macsMatch) {
|
|
if (macsMatch === false) {
|
|
throw 'MAC check failed.';
|
|
}
|
|
return window.crypto.subtle.importKey('raw', key.encKey.arr.buffer, keyOptions, false, ['decrypt']);
|
|
}).then(function (importedKey) {
|
|
return window.crypto.subtle.decrypt(decOptions, importedKey, cipher.ct.arr.buffer);
|
|
}).catch(function (err) {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
function computeMac(data, key) {
|
|
var alg = {
|
|
name: 'HMAC',
|
|
hash: { name: 'SHA-256' }
|
|
};
|
|
|
|
return window.crypto.subtle.importKey('raw', key, alg, false, ['sign'])
|
|
.then(function (importedKey) {
|
|
return window.crypto.subtle.sign(alg, importedKey, data);
|
|
});
|
|
}
|
|
|
|
function macsEqual(mac1Data, mac2Data, key) {
|
|
var alg = {
|
|
name: 'HMAC',
|
|
hash: { name: 'SHA-256' }
|
|
};
|
|
|
|
var mac1, importedMacKey;
|
|
return window.crypto.subtle.importKey('raw', key, alg, false, ['sign'])
|
|
.then(function (importedKey) {
|
|
importedMacKey = importedKey;
|
|
return window.crypto.subtle.sign(alg, importedMacKey, mac1Data);
|
|
}).then(function (mac) {
|
|
mac1 = mac;
|
|
return window.crypto.subtle.sign(alg, importedMacKey, mac2Data);
|
|
}).then(function (mac2) {
|
|
if (mac1.byteLength !== mac2.byteLength) {
|
|
return false;
|
|
}
|
|
|
|
var arr1 = new Uint8Array(mac1);
|
|
var arr2 = new Uint8Array(mac2);
|
|
|
|
for (var i = 0; i < arr2.length; i++) {
|
|
if (arr1[i] !== arr2[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function buildDataForMac(ivArr, ctArr) {
|
|
var dataForMac = new Uint8Array(ivArr.length + ctArr.length);
|
|
dataForMac.set(ivArr, 0);
|
|
dataForMac.set(ctArr, ivArr.length);
|
|
return dataForMac;
|
|
}
|
|
|
|
// App
|
|
|
|
var vm = new Vue({
|
|
el: '#app',
|
|
data: {
|
|
email: null,
|
|
masterPassword: null,
|
|
pbkdf2Iterations: 5000,
|
|
userKey: new ByteData(),
|
|
userKeyHash: new ByteData(),
|
|
key: new SymmetricCryptoKey(),
|
|
publicKey: null,
|
|
privateKey: null,
|
|
plaintext: '',
|
|
cipher: new Cipher(),
|
|
decPlaintext: ''
|
|
},
|
|
computed: {
|
|
masterPasswordBuffer: function () {
|
|
return this.masterPassword ? fromUtf8(this.masterPassword) : null;
|
|
},
|
|
emailBuffer: function () {
|
|
return this.email ? fromUtf8(this.email) : null;
|
|
},
|
|
plaintextBuffer: function () {
|
|
return this.plaintext ? fromUtf8(this.plaintext) : null;
|
|
}
|
|
},
|
|
watch: {
|
|
userKey: function (newUserKey) {
|
|
var self = this;
|
|
|
|
if (!newUserKey || !newUserKey.arr || !self.masterPasswordBuffer) {
|
|
return new ByteData();
|
|
}
|
|
|
|
pbkdf2(newUserKey.arr.buffer, self.masterPasswordBuffer, 1, 256).then(function (userKeyHash) {
|
|
self.userKeyHash = userKeyHash;
|
|
});
|
|
}
|
|
},
|
|
methods: {
|
|
generateKeys: function () {
|
|
var key = new Uint8Array(512 / 8);
|
|
window.crypto.getRandomValues(key);
|
|
this.key = new SymmetricCryptoKey(key);
|
|
}
|
|
}
|
|
});
|
|
|
|
vm.$watch(function () {
|
|
return {
|
|
masterPassword: vm.masterPasswordBuffer,
|
|
email: vm.emailBuffer,
|
|
iterations: vm.pbkdf2Iterations
|
|
};
|
|
}, function (newVal, oldVal) {
|
|
if (!newVal.masterPassword || !newVal.email || !newVal.iterations || newVal.iterations < 1) {
|
|
vm.userKey = new ByteData();
|
|
return;
|
|
}
|
|
|
|
pbkdf2(newVal.masterPassword, newVal.email, newVal.iterations, 256)
|
|
.then(function (userKey) {
|
|
vm.userKey = userKey;
|
|
});
|
|
});
|
|
|
|
vm.$watch(function () {
|
|
return {
|
|
key: vm.key,
|
|
plaintext: vm.plaintextBuffer
|
|
};
|
|
}, function (newVal, oldVal) {
|
|
if (!newVal.key || !newVal.plaintext) {
|
|
vm.cipher = new Cipher();
|
|
vm.decPlaintext = '';
|
|
return;
|
|
}
|
|
|
|
aesEncrypt(newVal.plaintext, newVal.key)
|
|
.then(function (cipher) {
|
|
vm.cipher = cipher;
|
|
return aesDecrypt(cipher, newVal.key);
|
|
}).then(function (plaintext) {
|
|
vm.decPlaintext = toUtf8(plaintext);
|
|
});
|
|
});
|
|
|
|
// Generate initial set of keys
|
|
vm.generateKeys();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|