Created
July 5, 2015 09:34
-
-
Save moxus/7789b93119df777465ea to your computer and use it in GitHub Desktop.
Decrypt KeePass files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global Buffer */ | |
/** | |
* Decrypt KeePass files | |
* | |
* There are some sources where you can find description about the keepass file format | |
* https://gist.githubusercontent.com/msmuenchen/9318327/raw/f6cbc07c01297f129700b7e14e1013253ab8b44f/gistfile1.txt | |
* http://blog.sharedmemory.fr/en/2014/04/30/keepass-file-format-explained/ | |
*/ | |
var FILE = 'test.kdbx'; | |
var PASSWORD = 'test'; | |
// a man needs what a man needs | |
var fs = require('fs'); | |
var crypto = require('crypto'); | |
var zlib = require('zlib'); | |
// read the testfile into a buffer | |
var fileHandle = fs.openSync(FILE, 'r'); | |
var fileSize = fs.fstatSync(fileHandle).size; | |
var b = new Buffer(fileSize); | |
fs.readSync(fileHandle, b, 0, fileSize); | |
/** | |
* 1. read the headers | |
*/ | |
var pos = 0; | |
var headers = {}; | |
// fixed position headers | |
headers.primaryIdentifier = b.slice(pos,pos+4); | |
pos += 4; | |
headers.secondaryIdentifier = b.slice(pos,pos+4); | |
pos += 4; | |
headers.fileVersionMinor = b.readUInt8(pos); | |
pos += 2; | |
headers.fileVersionMajor = b.readUInt8(pos); | |
pos += 2; | |
// TLV List (https://en.wikipedia.org/wiki/Type-length-value) | |
var TYPELEN = 1; | |
var LENGTHLEN = 2; | |
while(true){ | |
var type = b.readUInt8(pos); | |
pos += TYPELEN; | |
var len = b.readUInt16LE(pos); | |
pos += LENGTHLEN; | |
var val = b.slice(pos, pos+len); | |
pos += len; | |
headers[type] = val; | |
if(type === 0) { | |
headers.payloadArea = b.slice(pos); | |
break; | |
} | |
} | |
// interpret the raw header values | |
headers.cipherID = headers[2]; | |
headers.compression = headers[3].readUInt32LE(); | |
headers.masterSeed = headers[4]; | |
headers.transformSeed = headers[5]; | |
headers.transformRounds = headers[6].readUInt32LE(); // Definition says 64 bit | |
// node buffer dont support reading 64 bit integers. if you need the 64 bit check | |
// http://www.emoticode.net/javascript/node-js-read-and-convert-a-little-endian-64bit-integer-from-buffer-to-number.html | |
// for solutions. Be aware that the Cipher in this implementation is much slower than the one in KeePass. | |
headers.encryptionIV = headers[7]; | |
headers.protectedStreamKey = headers[8]; | |
headers.streamStartBytes = headers[9]; | |
headers.innerRandomStreamID = headers[10].readUInt32LE(); | |
console.log(headers); // shiny little headers | |
/** | |
* 2. create the master key | |
*/ | |
var hash = function (input) { | |
return crypto.createHash('sha256').update(input).digest(); | |
}; | |
var pw_hash = hash(PASSWORD); | |
var composite_key = hash(pw_hash); // other keys would be added here | |
var transformed_key = composite_key; | |
// var cipher = function (input) { | |
// var c = crypto.createCipheriv('aes-256-ecb', headers.transformSeed, new Buffer(0)); | |
// c.setAutoPadding(false); // should not make any difference since we use correct block sizes | |
// return Buffer.concat([c.update(input), c.final()]) | |
// }; | |
var createCipher = function () { | |
var c = crypto.createCipheriv('aes-256-ecb', headers.transformSeed, new Buffer(0)); | |
var cipher = function (input) { | |
return c.update(input); | |
}; | |
return cipher; | |
}; | |
var cipher = createCipher(); | |
for(var i=0; i<headers.transformRounds; i++) { | |
transformed_key = cipher(transformed_key); | |
} | |
transformed_key=hash(transformed_key); | |
var master_key = hash(Buffer.concat([headers.masterSeed, transformed_key])); | |
console.log('MASTER KEY: ' + master_key.toString('hex')); // you really should not log that in production | |
/** | |
* 3. get the data | |
*/ | |
var decrypt = function (input) { | |
var c = crypto.createDecipheriv('aes-256-cbc', master_key, headers.encryptionIV); | |
return Buffer.concat([c.update(input), c.final()]); | |
}; | |
var plainPayload = decrypt(headers.payloadArea); // isnt it easy | |
var correctDecoded = plainPayload.slice(0,headers.streamStartBytes.length).equals(headers.streamStartBytes); | |
console.log('correctDecoded ' + correctDecoded); | |
// there are multiple payload parts | |
var ppos = headers.streamStartBytes.length; | |
var payload = []; | |
while(ppos < plainPayload.length) { | |
var payloadBlock = {}; | |
payloadBlock.blockID = plainPayload.slice(ppos, ppos+4).readUInt32LE(); | |
ppos += 4; | |
payloadBlock.hash = plainPayload.slice(ppos, ppos+32); | |
ppos += 32; | |
payloadBlock.length = plainPayload.slice(ppos, ppos+4).readUInt32LE(); | |
ppos += 4; | |
payloadBlock.data = plainPayload.slice(ppos, ppos+payloadBlock.length); | |
ppos += payloadBlock.length; | |
if(payloadBlock.length > 0) { | |
payload.push(payloadBlock.data); | |
} | |
}; | |
// i am not sure about that, ther was always just one block | |
var data = Buffer.concat(payload); | |
if(headers.compression) { | |
console.log('DECOMPRESS'); | |
data = zlib.gunzipSync(data); | |
} | |
data = data.toString('utf8'); | |
console.log('DATA ' + data); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment