It seems like the brain-dead simplest KDF would have a key-stretching structure that is not substantially different from $\text{hash}(S | P | S | P |\dots|S|P)$, where $S$ is a salt and $P$ is a password. Other protocols generally require lots of hashes-of-hashes-of-hashes; this one builds up one big string to hash. This naive version has some obvious problems with timing (longer passwords require longer to verify), but it also might be easier to parallelize because the structure is perfectly repetitive, and it might "fall into a fixed point" of the hash function's compression-function, whereby further repetitions do not increase security.
The obvious thing to do, then, involves pre-hashing the password and incorporating a counter. Just to make this question 100% unambiguous, one implementation of this idea in Node.js would be:
function createKDF(work_factor, output_length, hash_algorithm) {
var crypto = require('crypto'), hash_length, output_blocks;
function hash(string) {
var context = crypto.createHash(hash_algorithm);
context.update(string, 'utf8');
return context.digest();
}
hash_length = hash("").length;
// needed to support arbitrary output lengths
output_blocks = Math.ceil(output_length / hash_length);
function output_transform(bin_data) {
var buff = new Buffer(hash_length * output_blocks), i, bstring;
bstring = bin_data.toString('hex');
for (i = 0; i < output_blocks; i += 1) {
hash(i + ':' + bstring).copy(buff, i * hash_length);
}
return buff.slice(0, output_length).toString('hex');
}
return {
createSalt: function () {
return crypto.randomBytes(hash_length - 4).toString('hex');
},
kdf: function (pass, salt) {
var hpass = hash('nodejs-passwords-0:' + work_factor + ':' + output_length + ':' + pass),
salt_buff = new Buffer(salt, 'hex'),
buff = new Buffer(2 * hash_length),
hash_state = crypto.createHash(hash_algorithm),
round;
assert(salt_buff.length + 4 == hash_length);
// buffer: [32-bit counter][(N - 32)-bit fixed salt][N-bit hpass]:
salt.copy(buff, 4);
hpass.copy(buff, hash_length);
for (round = 0; round < work_factor; round += 1) {
buff.writeUInt32LE(round, 0);
hash_state.update(buff);
}
return output_transform(hash_state.digest());
}
};
}
The reason I ask is that this approach seems simple enough that, if it is secure, the common advice of "don't roll your own crypto" is somewhat wrong: there are a variety of implementations of $$\text{key} = h_1(0|S|h_2(P)|1|S|h_2(P)|2|S|h_2(P)\dots)$$which you can easily program to create a password-storage system which suits your particular application, if something like PBKDF2 or bcrypt or scrypt is not available in your programming environment.
(Of course Node.js has PBKDF2, so the above implementation is more to clarify the question of "what's wrong with this general approach?" rather than it is to be useful to a JS programmer.)