You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
577 lines
21 KiB
577 lines
21 KiB
<!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://stackpath.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://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"> |
|
|
|
<style> |
|
body { |
|
padding-bottom: 50px; |
|
} |
|
|
|
h1 { |
|
border-bottom: 2px solid #ced4da; |
|
margin-bottom: 20px; |
|
padding-bottom: 10px; |
|
margin-top: 50px; |
|
} |
|
|
|
h2 { |
|
font-size: 24px; |
|
} |
|
|
|
h3 { |
|
font-size: 18px; |
|
} |
|
|
|
pre { |
|
padding: 9.5px; |
|
line-height: 1.42857143; |
|
word-break: break-all; |
|
word-wrap: break-word; |
|
background-color: #f5f5f5; |
|
border: 1px solid #ced4da; |
|
border-radius: 4px; |
|
} |
|
|
|
section { |
|
margin-bottom: 50px; |
|
} |
|
</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> |
|
|
|
<section> |
|
<h2>Master Key</h2> |
|
<pre>{{masterKey.b64}}</pre> |
|
</section> |
|
|
|
<section> |
|
<h2>Master Password Hash</h2> |
|
<pre>{{masterKeyHash.b64}}</pre> |
|
</section> |
|
|
|
<section> |
|
<h2>Stretched Master Key</h2> |
|
<pre>{{stretchedMasterKey.key.b64}}</pre> |
|
<h3>Encryption Key</h3> |
|
<pre>{{stretchedMasterKey.encKey.b64}}</pre> |
|
<h3>MAC Key</h3> |
|
<pre>{{stretchedMasterKey.macKey.b64}}</pre> |
|
</section> |
|
|
|
<section> |
|
<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> |
|
</section> |
|
|
|
<section> |
|
<h2>Generated RSA Key Pair</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> |
|
</section> |
|
|
|
<button type="button" id="deriveKeys" class="btn btn-primary" v-on:click="generateKeys"> |
|
<i class="fa fa-refresh"></i> Regenerate Keys |
|
</button> |
|
|
|
<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 |
|
}; |
|
|
|
// Object 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 |
|
|
|
async 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 |
|
}; |
|
|
|
try { |
|
const importedKey = await window.crypto.subtle.importKey( |
|
'raw', password, importAlg, false, ['deriveKey']); |
|
const derivedKey = await window.crypto.subtle.deriveKey( |
|
deriveAlg, importedKey, aesOptions, true, ['encrypt']); |
|
const exportedKey = await window.crypto.subtle.exportKey('raw', derivedKey); |
|
return new ByteData(exportedKey); |
|
} catch (err) { |
|
console.log(err); |
|
} |
|
} |
|
|
|
async 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); |
|
|
|
try { |
|
const importedKey = await window.crypto.subtle.importKey( |
|
'raw', encKey.arr.buffer, keyOptions, false, ['encrypt']); |
|
const encryptedBuffer = await window.crypto.subtle.encrypt(encOptions, importedKey, data); |
|
const ctData = new ByteData(encryptedBuffer); |
|
let type = encTypes.AesCbc256_B64; |
|
let macData; |
|
if (macKey) { |
|
const dataForMac = buildDataForMac(ivData.arr, ctData.arr); |
|
const macBuffer = await computeMac(dataForMac.buffer, macKey.arr.buffer); |
|
type = encTypes.AesCbc256_HmacSha256_B64; |
|
macData = new ByteData(macBuffer); |
|
} |
|
return new Cipher(type, ivData, ctData, macData); |
|
} catch (err) { |
|
console.error(err); |
|
} |
|
} |
|
|
|
async function aesDecrypt(cipher, encKey, macKey) { |
|
const keyOptions = { |
|
name: 'AES-CBC' |
|
}; |
|
|
|
const decOptions = { |
|
name: 'AES-CBC', |
|
iv: cipher.iv.arr.buffer |
|
}; |
|
|
|
try { |
|
const checkMac = cipher.encType != encTypes.AesCbc256_B64; |
|
if (checkMac) { |
|
if (!macKey) { |
|
throw 'MAC key not provided.'; |
|
} |
|
const dataForMac = buildDataForMac(cipher.iv.arr, cipher.ct.arr); |
|
const macBuffer = await computeMac(dataForMac.buffer, macKey.arr.buffer); |
|
const macsMatch = await macsEqual(cipher.mac.arr.buffer, macBuffer, macKey.arr.buffer); |
|
if (!macsMatch) { |
|
throw 'MAC check failed.'; |
|
} |
|
const importedKey = await window.crypto.subtle.importKey( |
|
'raw', encKey.arr.buffer, keyOptions, false, ['decrypt']); |
|
return window.crypto.subtle.decrypt(decOptions, importedKey, cipher.ct.arr.buffer); |
|
} |
|
} catch (err) { |
|
console.error(err); |
|
} |
|
} |
|
|
|
async function computeMac(data, key) { |
|
const alg = { |
|
name: 'HMAC', |
|
hash: { name: 'SHA-256' } |
|
}; |
|
const importedKey = await window.crypto.subtle.importKey('raw', key, alg, false, ['sign']); |
|
return window.crypto.subtle.sign(alg, importedKey, data); |
|
} |
|
|
|
async function macsEqual(mac1Data, mac2Data, key) { |
|
const alg = { |
|
name: 'HMAC', |
|
hash: { name: 'SHA-256' } |
|
}; |
|
|
|
const importedMacKey = await window.crypto.subtle.importKey('raw', key, alg, false, ['sign']); |
|
const mac1 = await window.crypto.subtle.sign(alg, importedMacKey, mac1Data); |
|
const mac2 = await window.crypto.subtle.sign(alg, importedMacKey, mac2Data); |
|
|
|
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; |
|
} |
|
|
|
async function generateRsaKeyPair() { |
|
const rsaOptions = { |
|
name: 'RSA-OAEP', |
|
modulusLength: 2048, |
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 |
|
hash: { name: 'SHA-1' } |
|
}; |
|
|
|
try { |
|
const keyPair = await window.crypto.subtle.generateKey(rsaOptions, true, ['encrypt', 'decrypt']); |
|
const publicKey = new ByteData(await window.crypto.subtle.exportKey('spki', keyPair.publicKey)); |
|
const privateKey = new ByteData(await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey)); |
|
return { |
|
publicKey: publicKey, |
|
privateKey: privateKey |
|
}; |
|
} catch (err) { |
|
console.error(err); |
|
} |
|
} |
|
|
|
async function stretchKey(key) { |
|
const newKey = new Uint8Array(64); |
|
newKey.set(await hkdfExpand(key, new Uint8Array(fromUtf8('enc')), 32)); |
|
newKey.set(await hkdfExpand(key, new Uint8Array(fromUtf8('mac')), 32), 32); |
|
return new SymmetricCryptoKey(newKey.buffer); |
|
} |
|
|
|
// ref: https://tools.ietf.org/html/rfc5869 |
|
async function hkdfExpand(prk, info, size) { |
|
const alg = { |
|
name: 'HMAC', |
|
hash: { name: 'SHA-256' } |
|
}; |
|
const importedKey = await window.crypto.subtle.importKey('raw', prk, alg, false, ['sign']); |
|
const hashLen = 32; // sha256 |
|
const okm = new Uint8Array(size); |
|
let previousT = new Uint8Array(0); |
|
const n = Math.ceil(size / hashLen); |
|
for (let i = 0; i < n; i++) { |
|
const t = new Uint8Array(previousT.length + info.length + 1); |
|
t.set(previousT); |
|
t.set(info, previousT.length); |
|
t.set([i + 1], t.length - 1); |
|
previousT = new Uint8Array(await window.crypto.subtle.sign(alg, importedKey, t.buffer)); |
|
okm.set(previousT, i * hashLen); |
|
} |
|
return okm; |
|
} |
|
|
|
// App |
|
|
|
const vm = new Vue({ |
|
el: '#app', |
|
data: { |
|
email: '', |
|
masterPassword: '', |
|
pbkdf2Iterations: 100000, |
|
|
|
masterKey: new ByteData(), |
|
masterKeyHash: new ByteData(), |
|
stretchedMasterKey: new SymmetricCryptoKey(), |
|
|
|
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: { |
|
async masterKey(newValue) { |
|
const self = this; |
|
|
|
if (!newValue || !newValue.arr || !self.masterPasswordBuffer) { |
|
return new ByteData(); |
|
} |
|
|
|
self.stretchedMasterKey = await stretchKey(newValue.arr.buffer); |
|
self.masterKeyHash = await pbkdf2(newValue.arr.buffer, self.masterPasswordBuffer, 1, 256); |
|
} |
|
}, |
|
methods: { |
|
async generateKeys() { |
|
const self = this; |
|
|
|
const symKey = new Uint8Array(512 / 8); |
|
window.crypto.getRandomValues(symKey); |
|
self.symKey = new SymmetricCryptoKey(symKey); |
|
|
|
const keyPair = await generateRsaKeyPair(); |
|
self.publicKey = keyPair.publicKey; |
|
self.privateKey = keyPair.privateKey; |
|
} |
|
} |
|
}); |
|
|
|
vm.$watch(() => { |
|
return { |
|
masterPassword: vm.masterPasswordBuffer, |
|
email: vm.emailBuffer, |
|
iterations: vm.pbkdf2Iterations |
|
}; |
|
}, async (newVal, oldVal) => { |
|
if (!newVal.masterPassword || !newVal.email || !newVal.iterations || newVal.iterations < 1) { |
|
vm.masterKey = new ByteData(); |
|
return; |
|
} |
|
|
|
vm.masterKey = await pbkdf2(newVal.masterPassword, newVal.email, newVal.iterations, 256); |
|
}); |
|
|
|
vm.$watch(() => { |
|
return { |
|
symKey: vm.symKey, |
|
secret: vm.secretBuffer |
|
}; |
|
}, async (newVal, oldVal) => { |
|
if (!newVal.symKey || !newVal.secret) { |
|
vm.protectedSecret = new Cipher(); |
|
vm.unprotectedSecret = ''; |
|
return; |
|
} |
|
|
|
vm.protectedSecret = await aesEncrypt(newVal.secret, newVal.symKey.encKey, newVal.symKey.macKey); |
|
const secret = await aesDecrypt(vm.protectedSecret, newVal.symKey.encKey, newVal.symKey.macKey); |
|
vm.unprotectedSecret = toUtf8(secret); |
|
}); |
|
|
|
vm.$watch(() => { |
|
return { |
|
stretchedMasterKey: vm.stretchedMasterKey, |
|
symKey: vm.symKey |
|
}; |
|
}, async (newVal, oldVal) => { |
|
if (!newVal.stretchedMasterKey || !newVal.stretchedMasterKey.key || |
|
!newVal.stretchedMasterKey.key.arr || !newVal.symKey || !newVal.symKey.key || |
|
!newVal.symKey.key.arr) { |
|
vm.protectedSymKey = new Cipher(); |
|
return; |
|
} |
|
|
|
vm.protectedSymKey = await aesEncrypt(newVal.symKey.key.arr, newVal.stretchedMasterKey.encKey, |
|
newVal.stretchedMasterKey.macKey); |
|
const unprotectedSymKey = await aesDecrypt(vm.protectedSymKey, newVal.stretchedMasterKey.encKey, |
|
newVal.stretchedMasterKey.macKey); |
|
vm.unprotectedSymKey = new ByteData(unprotectedSymKey); |
|
}); |
|
|
|
vm.$watch(() => { |
|
return { |
|
symKey: vm.symKey, |
|
privateKey: vm.privateKey |
|
}; |
|
}, async (newVal, oldVal) => { |
|
if (!newVal.symKey || !newVal.symKey.key || !newVal.privateKey || !newVal.privateKey.arr) { |
|
vm.protectedPrivateKey = new Cipher(); |
|
return; |
|
} |
|
|
|
vm.protectedPrivateKey = await aesEncrypt(newVal.privateKey.arr, newVal.symKey.encKey, |
|
newVal.symKey.macKey); |
|
const unprotectedPrivateKey = await aesDecrypt(vm.protectedPrivateKey, newVal.symKey.encKey, |
|
newVal.symKey.macKey); |
|
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> |