Skip to content

Instantly share code, notes, and snippets.

@moxus
Created July 5, 2015 09:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moxus/7789b93119df777465ea to your computer and use it in GitHub Desktop.
Save moxus/7789b93119df777465ea to your computer and use it in GitHub Desktop.
Decrypt KeePass files
/* 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