I'm writing code to read/write to NTAG424 DNA NFC tags. I'm doing this completely in javascript, because I want to be able to use it in a react native app.
In NTAG 424 DNA and NTAG 424 DNA TagTamper features and hints they show the results you should be getting for every step. But they use a python solution.
The input message is A55A0001008013C56268A548D8FBBF237CCCAA20EC7E6E48C3DEF9A4C675360F and the output (according to the manual) is 1309C877509E5A215007FF0ED19CA564. Whereas I get 7CCEF6FEB32F34CA48CB685ECAA0F32C.
Because I need to be able to use this code commercially, I cannot just use any library.
function generateSubkeys(key) {
const cipher = crypto.createCipheriv("aes-128-ecb", key, "");
const cryptedKey = cipher.update(iv);
let subkey1 = bt.bitShiftLeft(cryptedKey);
if (msb(cryptedKey[0]) & 0x80) {
subkey1 = xor(subkey1, 0x87);
}
let subkey2 = bt.bitShiftLeft(subkey1);
if (msb(subkey1[0]) & 0x80) {
subkey2 = xor(subkey2, 0x87);
}
return { subkey1: subkey1, subkey2: subkey2 };
}
function msb(bytes) {
return bytes >>> 31;
}
function aes(key, message) {
const cipher = crypto.createCipheriv(
"aes-" + key.length * 8 + "-cbc",
key,
iv
);
var result = cipher.update(message);
cipher.final();
return result;
}
function aesCmac(key, message) {
const { subkey1, subkey2 } = generateSubkeys(Buffer.from(key, "hex"));
let numBlocks = Math.ceil(message.length / blockSize);
var lastBlockRemainder = message.length % blockSize;
if (numBlocks === 0) {
numBlocks = 1;
}
var messageArray = getMessageArray(message, numBlocks, lastBlockRemainder);
if (lastBlockRemainder === 0) {
messageArray[numBlocks - 1] = xor(messageArray[numBlocks - 1], subkey1);
} else {
messageArray[numBlocks - 1] = xor(messageArray[numBlocks - 1], subkey2);
}
var c = aes(
key,
Buffer.concat(messageArray.slice(0, messageArray.length - 1))
);
let c_xor_m = xor(c, messageArray[messageArray.length - 1]);
c = aes(key, c_xor_m);
return c;
}
function getMessageArray(message, numBlocks, lastBlockRemainder) {
var index = 0;
var messageArray = [];
if (lastBlockRemainder !== 0) {
let padding = "80" + "00".repeat(16 - lastBlockRemainder - 1);
let appendToMessage = Buffer.from(padding, "hex");
message = Buffer.concat([message, appendToMessage]);
}
for (index = 0; index < numBlocks; index++) {
let messageBlock = message.slice(
index * blockSize,
(index + 1) * blockSize
);
messageArray.push(messageBlock);
}
return messageArray;
}
I already tried the one mentioned here AES-CMAC module for Node.js? and completely rewriting the code to my own version of an AES-CMAC algorithm. In both the one I tried and the one I made (with the help of NIST Special Publication 800-38B), I get the same results.
Now I'm stuck between thinking either my code is wrong, or the python crypto library (where I don't completely understand the code) is wrong.
Can anyone help me figure out which of the two is true? And in case my code is wrong, help me fix it.
I found the answer: The Crypto library in javascript has an aes-cbc cipher, that says (and it does) accepts buffers and arrays. But the outcomes of both are different.
When I used a UInt8Array I got the right outcome. I completely rewrote an aes-cmac algorithm, just to figure out this what all I needed.
Related
I took the mother of all random number generators from Agner Fog's library and attempted to make a JavaScript version of this. I expect the bit pattern to be identical but this doesn't seem to be the case and I wonder why or if I made a mistake?
The C++ code I use can be found here. It's from Agner Fog's website.
Here's my TypeScript version of this code
const u = (function () {
const b = new ArrayBuffer(8)
return {
u32: new Uint32Array(b),
u64: new BigUint64Array(b),
}
})()
export class Random {
state = new Uint32Array(5)
constructor(seed?: number) {
this.seed(seed ?? +new Date())
}
b() {
const { state: x } = this
u.u64[0] =
2111111111n * BigInt(x[3]) +
1492n * BigInt(x[2]) +
1776n * BigInt(x[1]) +
5115n * BigInt(x[0]) +
BigInt(x[4])
console.debug(u.u64[0])
x[3] = x[2]
x[2] = x[1]
x[1] = x[0]
x[4] = u.u32[1]
x[0] = u.u32[0]
return x[0]
}
next() {
return (1 / 4_294_967_296) * this.b()
}
seed(seed: number) {
const { state: x } = this
let s = seed >>> 0
// make random numbers and put them into the buffer
for (let i = 0; i < 5; i++) {
s = s * 29943829 - 1
x[i] = s
}
console.debug(x)
// randomize some more
for (let i = 0; i < 19; i++) {
this.b()
}
}
}
I managed to get it down to the the state initialization but I cannot understand why the output from the C++ program is 4294967295, 4265023466, 1627457073, 3925469700, 2226377299 but the TypeScript program is giving me 4294967295, 4265023466, 1627457073, 3925868544, 0. Only the first 3 numbers get computed exactly the same. Any help trying to understand this is much appreciated.
Ah, I figured it out. I have to use Math.imul to get correct results. Can't do C-like integer multiplication without it.
So, this:
for (let i = 0; i < 5; i++) {
s = s * 29943829 - 1
x[i] = s
}
...changes into:
for (let i = 0; i < 5; i++) {
s = Math.imul(s, 29943829) - 1
x[i] = s
}
I tried stuff like (s * 29943829) >>> 0 but this is not enough to force C style integer multiplication.
I am setting up a google spreadsheet project to connect to my CryptoExchange
API.
But when it comes to this simple CryptoJs Hmac-sha256 script, it's not working: it is returning the function structure instead of the value, while outside it's working fine (see my jsfiddle).
Now, I understand from this Stack answer by Cameron Roberts that Apps Script behaves differently under certain POVs, but I can't understand how this relates.
Besides, if I just switch script and use the Stanford Javascript Crypto
Library, the code executes perfectly with no issue at all, both within Google
Apps Script AND outside of it of course.
Here is my code:
eval(UrlFetchApp.fetch('https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/hmac-sha256.js').getContentText());
function write() {
var hash = CryptoJS.HmacSHA256("message", "secret");
return hash;
}
Logger.log(write());
and the console log from Google Apps Script
[19-06-07 00:53:32:859 PDT] {mixIn=
function (a) {
for (var c in a) {
a.hasOwnProperty(c) && (this[c] = a[c]);
}
a.hasOwnProperty("toString") && (this.toString = a.toString);
}
, extend=
function (a) {
q.prototype = this;
var c = new q();
a && c.mixIn(a);
c.hasOwnProperty("init") || (c.init = function () {
c.$super.init.apply(this, arguments);
});
c.init.prototype = c;
c.$super = this;
return c;
}
, init=
function (a, c) {
a = this.words = a || [];
this.sigBytes = c != s ? c : 4 * a.length;
}
, random=
function (a) {
for (var c = [], d = 0; d < a; d += 4) {
c.push(4294967296 * h.random() | 0);
}
return new r.init(c, a);
}
, words=[-1.956689808E9, 6.97680217E8, -1.940439631E9, -5.01717335E8, -
1.205480281E9, -1.798215209E9, 1.0131952E8, 1.469462027E9], clone=
function () {
var a = m.clone.call(this);
a.words = this.words.slice(0);
return a;
}
, sigBytes=32.0, create=
function () {
var a = this.extend();
a.init.apply(a, arguments);
return a;
}
, toString=
function (a) {
return (a || k).stringify(this);
}
, concat=
function (a) {
var c = this.words, d = a.words, b = this.sigBytes;
a = a.sigBytes;
this.clamp();
if (b % 4) {
for (var e = 0; e < a; e++) {
c[b + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 -
8 * ((b + e) % 4);
}
} else {
if (65535 < d.length) {
for (e = 0; e < a; e += 4) {
c[b + e >>> 2] = d[e >>> 2];
}
} else {
c.push.apply(c, d);
}
}
this.sigBytes += a;
return this;
}
, clamp=
function () {
var a = this.words, c = this.sigBytes;
a[c >>> 2] &= 4294967295 << 32 - 8 * (c % 4);
a.length = h.ceil(c / 4);
}
, $super={extend=
function (a) {
q.prototype = this;
var c = new q();
a && c.mixIn(a);
c.hasOwnProperty("init") || (c.init = function () {
c.$super.init.apply(this, arguments);
});
c.init.prototype = c;
c.$super = this;
return c;
}
, mixIn=
function (a) {
for (var c in a) {
a.hasOwnProperty(c) && (this[c] = a[c]);
}
a.hasOwnProperty("toString") && (this.toString = a.toString);
}
, init=
function () {
}
, clone=
function () {
return this.init.prototype.extend(this);
}
, create=
function () {
var a = this.extend();
a.init.apply(a, arguments);
return a;
}
}}
While the same code within jsfiddle works fine
EDIT:
While my question is still a source of curiosity for me, I have just found a whole branch of replies here on stack which involve a specific method within Google Apps Script I didn't know about: a built in Class Utility for creating HMAC Sha256 signature.
This may not be the very answer to my question in terms of theoretical knowledge, but will probably solve my problem from a practical point of view; so I will look into that now.
Thanks
Generate a keyed hash value using the HMAC method with Google Apps Script
How to get Hex value from computeHmacSha256Signature method of Google Apps Script?
get back a string representation from computeDigest(algorithm, value) byte[]
You want to retrieve the value of 8b5f48702995c1598c573db1e21866a9b825d4a794d169d7060a03605796360b from CryptoJS.HmacSHA256("message", "secret") using Google Apps Script.
If my understanding is correct, how about directly calculating the value using the methods of Google Apps Script? In this case, CryptoJS is not used. Please think of this as just one of several answers.
Sample script:
var res = Utilities.computeHmacSha256Signature("message", "secret")
.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
Logger.log(res)
Result:
8b5f48702995c1598c573db1e21866a9b825d4a794d169d7060a03605796360b
Note:
The important point for above script is as follows.
At Google Apps Script, the data which was encrypted by Utilities.computeHmacSha256Signature() is the bytes array of the signed hexadecimal.
In your case, the bytes array is converted to the unsigned hexadecimal.
References:
computeHmacSha256Signature(value, key)
map()
If I misunderstood your question and this was not the direction you want, I apologize.
So I am looking to create look up tables. However I am running into a problem with integer ranges instead of just 1, 2, 3, etc. Here is what I have:
var ancient = 1;
var legendary = 19;
var epic = 251;
var rare = 1000;
var uncommon = 25000;
var common = 74629;
var poolTotal = ancient + legendary + epic + rare + uncommon + common;
var pool = general.rand(1, poolTotal);
var lootPool = {
1: function () {
return console.log("Ancient");
},
2-19: function () {
}
};
Of course I know 2-19 isn't going to work, but I've tried other things like [2-19] etc etc.
Okay, so more information:
When I call: lootPool[pool](); It will select a integer between 1 and poolTotal Depending on if it is 1 it will log it in the console as ancient. If it hits in the range of 2 through 19 it would be legendary. So on and so forth following my numbers.
EDIT: I am well aware I can easily do this with a switch, but I would like to try it this way.
Rather than making a huge lookup table (which is quite possible, but very inelegant), I'd suggest making a (small) object, choosing a random number, and then finding the first entry in the object whose value is greater than the random number:
// baseLootWeight: weights are proportional to each other
const baseLootWeight = {
ancient: 1,
legendary: 19,
epic: 251,
rare: 1000,
uncommon: 25000,
common: 74629,
};
let totalWeightSoFar = 0;
// lootWeight: weights are proportional to the total weight
const lootWeight = Object.entries(baseLootWeight).map(([rarity, weight]) => {
totalWeightSoFar += weight;
return { rarity, weight: totalWeightSoFar };
});
console.log(lootWeight);
const randomType = () => {
const rand = Math.floor(Math.random() * totalWeightSoFar);
return lootWeight
.find(({ rarity, weight }) => weight >= rand)
.rarity;
};
for (let i = 0; i < 10; i++) console.log(randomType());
Its not a lookup, but this might help you.
let loots = {
"Ancient": 1,
"Epic": 251,
"Legendary": 19
};
//We need loots sorted by value of lootType
function prepareSteps(loots) {
let steps = Object.entries(loots).map((val) => {return {"lootType": val[0], "lootVal": val[1]}});
steps.sort((a, b) => a.lootVal > b.lootVal);
return steps;
}
function getMyLoot(steps, val) {
let myLootRange;
for (var i = 0; i < steps.length; i++) {
if((i === 0 && val < steps[0].lootVal) || val === steps[i].lootVal) {
myLootRange = steps[i];
break;
}
else if( i + 1 < steps.length && val > steps[i].lootVal && val < steps[i + 1].lootVal) {
myLootRange = steps[i + 1];
break;
}
}
myLootRange && myLootRange['lootType'] ? console.log(myLootRange['lootType']) : console.log('Off Upper Limit!');
}
let steps = prepareSteps(loots);
let pool = 0;
getMyLoot(steps, pool);
I am currently using FreeCodeCamp to try to learn basic JavaScript scripting. The problem that I am currently working on is:
http://www.freecodecamp.com/challenges/bonfire-map-the-debris.
The problem involves using OOP to solve a specific task (calculating orbital periods from the given altitude).
My code is as follows:
function orbitalPeriod(arr) {
var GM = 398600.4418;
var earthRadius = 6367.4447;
this.arr = arr;
for(var i = 0; i < arr.length; i++){
var altitude = this.arr[i]["avgAlt"] + earthRadius;
var calc = Math.round((2*Math.PI) * Math.sqrt(Math.pow(altitude,3) / GM),1);
this.arr[i]["avgAlt"] = calc;
}
return this.arr;
}
orbitalPeriod([{name : "sputkin", avgAlt : 35873.5553}]);
The issue is not with my calculations. Rather, when I submit my code, I get: "expected [ { name: 'sputkin', avgAlt: 86400 } ] to deeply equal [ Array (1) ]". Does anyone know why it is telling me that I should return an Array (1)?
The test suite is expecting the return array to contain an object with the properties name and orbitalPeriod - yours is returning an array containing an object with the properties name and avgAlt.
Side note, don't use the this keyword unless you're sure as to what it does - and I promise you it does not do what you think it does here.
Here's the solution, compare it with yours. Your calculations were correct, so good job on that part.
function orbitalPeriod(arr) {
var GM = 398600.4418,
earthRadius = 6367.4447,
output = [], altitude, calc;
for (var i = 0; i < arr.length; i++){
altitude = arr[i].avgAlt + earthRadius;
calc = Math.round((2*Math.PI) * Math.sqrt(Math.pow(altitude,3) / GM));
output.push({
name: arr[i].name,
orbitalPeriod: calc
});
}
return output;
}
orbitalPeriod([{name : "sputkin", avgAlt : 35873.5553}]);
Bonus note: Math.round() only takes one parameter.
Bonus answer:
Array.prototype.map() makes this super clean, if we're not tuning for performance.
function orbitalPeriod(arr) {
var GM = 398600.4418,
earthRadius = 6367.4447;
return arr.map(function (o) {
return {
name: o.name,
orbitalPeriod: Math.round((2 * Math.PI) * Math.sqrt(Math.pow(o.avgAlt + earthRadius, 3) / GM))
};
});
}
orbitalPeriod([{name : "sputkin", avgAlt : 35873.5553}]);
EDIT: Per discussion in the comments, let me clarify that this will be happening server side, behind SSL. I do not intend to expose the hashed password or the hashing scheme to the client.
Assume we have an existing asp.net identity database with the default tables (aspnet_Users, aspnet_Roles, etc.). Based on my understanding, the password hashing algorithm uses sha256 and stores the salt + (hashed password) as a base64 encoded string. EDIT: This assumption is incorrect, see answer below.
I would like to replicate the function of the Microsoft.AspNet.Identity.Crypto class' VerifyHashedPassword function with a JavaScript version.
Let's say that a password is welcome1 and its asp.net hashed password is ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==
So far I have been able to reproduce the parts of the method that get the salt and the stored sub key.
Where the C# implementation does more or less this:
var salt = new byte[SaltSize];
Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
var storedSubkey = new byte[PBKDF2SubkeyLength];
Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PBKDF2SubkeyLength);
I have the following in JavaScript (not elegant by any stretch):
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var saltbytes = [];
var storedSubKeyBytes = [];
for(var i=1;i<hashedPasswordBytes.length;i++)
{
if(i > 0 && i <= 16)
{
saltbytes.push(hashedPasswordBytes[i]);
}
if(i > 0 && i >16) {
storedSubKeyBytes.push(hashedPasswordBytes[i]);
}
}
Again, it ain't pretty, but after running this snippet the saltbytes and storedSubKeyBytes match byte for byte what I see in the C# debugger for salt and storedSubkey.
Finally, in C#, an instance of Rfc2898DeriveBytes is used to generate a new subkey based on the salt and the password provided, like so:
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
{
generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
This is where I'm stuck. I have tried others' solutions such as this one, I have used Google's and Node's CryptoJS and crypto libraries respectively, and my output never generates anything resembling the C# version.
(Example:
var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'),
new Buffer(parsedSaltString), 1000, 32, 'sha256');
console.log(output.toString('base64'))
generates "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY=")
Many of the pointers I've found online indicate problems involving encoding mismatches (NodeJS / UTF-8 vs. .NET / UTF-16LE), so I've tried encoding using the default .NET encoding format but to no avail.
Or I could be completely wrong about what I assume these libraries are doing. But any pointers in the right direction would be much appreciated.
Ok, I think this problem ended up being quite a bit simpler than I was making it (aren't they always). After performing a RTFM operation on the pbkdf2 spec, I ran some side-by-side tests with Node crypto and .NET crypto, and have made pretty good progress on a solution.
The following JavaScript code correctly parses the stored salt and subkey, then verifies the given password by hashing it with the stored salt. There are doubtless better / cleaner / more secure tweaks, so comments welcome.
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored in [dbo].[AspNetUsers].[PasswordHash]
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
var saltString = "";
var storedSubKeyString = "";
// build strings of octets for the salt and the stored key
for (var i = 1; i < hashedPasswordBytes.length; i++) {
if (i > 0 && i <= 16) {
saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]
}
if (i > 0 && i > 16) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// This is where the magic happens.
// If you are doing your own hashing, you can (and maybe should)
// perform more iterations of applying the salt and perhaps
// use a stronger hash than sha1, but if you want it to work
// with the [as of 2015] Microsoft Identity framework, keep
// these settings.
var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1');
// get a hex string of the derived bytes
var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase();
console.log("hex of derived key octets: " + derivedKeyOctets);
// The first 64 bytes of the derived key should
// match the stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
The previous solution will not work in all cases.
Let's say that you want to compare a password source against a hash in the database hash, which can be technically possible if the database is compromised, then the function will return true because the subkey is an empty string.
Modify the function to catch that up and return false instead.
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored in [dbo].[AspNetUsers].[PasswordHash]
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
var saltString = "";
var storedSubKeyString = "";
// build strings of octets for the salt and the stored key
for (var i = 1; i < hashedPasswordBytes.length; i++) {
if (i > 0 && i <= 16) {
saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]
}
if (i > 0 && i > 16) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
if (storedSubKeyString === '') { return false }
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// This is where the magic happens.
// If you are doing your own hashing, you can (and maybe should)
// perform more iterations of applying the salt and perhaps
// use a stronger hash than sha1, but if you want it to work
// with the [as of 2015] Microsoft Identity framework, keep
// these settings.
var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1');
// get a hex string of the derived bytes
var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase();
console.log("hex of derived key octets: " + derivedKeyOctets);
// The first 64 bytes of the derived key should
// match the stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
Here's another option which actually compares the bytes as opposed to converting to a string representation.
const crypto = require('crypto');
const password = 'Password123';
const storedHashString = 'J9IBFSw0U1EFsH/ysL+wak6wb8s=';
const storedSaltString = '2nX0MZPZlwiW8bYLlVrfjBYLBKM=';
const storedHashBytes = new Buffer.from(storedHashString, 'base64');
const storedSaltBytes = new Buffer.from(storedSaltString, 'base64');
crypto.pbkdf2(password, storedSaltBytes, 1000, 20, 'sha1',
(err, calculatedHashBytes) => {
const correct = calculatedHashBytes.equals(storedHashBytes);
console.log('Password is ' + (correct ? 'correct 😎' : 'incorrect ðŸ˜'));
}
);
1000 is the default number of iterations in System.Security.Cryptography.Rfc2898DeriveBytes and 20 is the number of bytes we are using to store the salt (again the default).
I know this is rather late, but I ran into an issue with reproducing C#'s Rfc2898DeriveBytes.GetBytes in Node, and kept coming back to this SO answer. I ended up creating a minimal class for my own usage, and I figured I'd share in case someone else was having the same issues. It's not perfect, but it works.
const crypto = require('crypto');
const $key = Symbol('key');
const $saltSize = Symbol('saltSize');
const $salt = Symbol('salt');
const $iterationCount = Symbol('iterationCount');
const $position = Symbol('position');
class Rfc2898DeriveBytes {
constructor(key, saltSize = 32, iterationCount = 1000) {
this[$key] = key;
this[$saltSize] = saltSize;
this[$iterationCount] = iterationCount;
this[$position] = 0;
this[$salt] = crypto.randomBytes(this[$saltSize]);
}
get salt() {
return this[$salt];
}
set salt(buffer) {
this[$salt] = buffer;
}
get iterationCount() {
return this[$iterationCount];
}
set iterationCount(count) {
this[$iterationCount] = count;
}
getBytes(byteCount) {
let position = this[$position];
let bytes = crypto.pbkdf2Sync(Buffer.from(this[$key]), this.salt, this.iterationCount, position + byteCount, 'sha1');
this[$position] += byteCount;
let result = Buffer.alloc(byteCount);
for (let i = 0; i < byteCount; i++) { result[i] = bytes[position + i]; }
return result;
}
}
module.exports = Rfc2898DeriveBytes;