How can I get HMAC-SHA512(key, data) in the browser using Crypto Web API (window.crypto)?
Currently I am using CryptoJS library and it is pretty simple:
CryptoJS.HmacSHA512("myawesomedata", "mysecretkey").toString();
Result is 91c14b8d3bcd48be0488bfb8d96d52db6e5f07e5fc677ced2c12916dc87580961f422f9543c786eebfb5797bc3febf796b929efac5c83b4ec69228927f21a03a.
I want to get rid of extra dependencies and start using Crypto Web API instead. How can I get the same result with it?
Answering my own question. The code below returns the same result as CryptoJS.HmacSHA512("myawesomedata", "mysecretkey").toString();
There are promises everywhere as WebCrypto is asynchronous:
// encoder to convert string to Uint8Array
var enc = new TextEncoder("utf-8");
window.crypto.subtle.importKey(
"raw", // raw format of the key - should be Uint8Array
enc.encode("mysecretkey"),
{ // algorithm details
name: "HMAC",
hash: {name: "SHA-512"}
},
false, // export = false
["sign", "verify"] // what this key can do
).then( key => {
window.crypto.subtle.sign(
"HMAC",
key,
enc.encode("myawesomedata")
).then(signature => {
var b = new Uint8Array(signature);
var str = Array.prototype.map.call(b, x => x.toString(16).padStart(2, '0')).join("")
console.log(str);
});
});
Async/Await Crypto Subtle HMAC SHA-256/512 with Base64 Digest
The following is a copy of the ✅ answer. This time we are using async/await for clean syntax. This approach also offers a base64 encoded digest.
secret is the secret key that will be used to sign the body.
body is the string-to-sign.
enc is a text encoder that converts the UTF-8 to JavaScript byte arrays.
algorithm is a JS object which is used to identify the signature methods.
key is a CryptoKey.
signature is the byte array hash.
digest is the base64 encoded signature.
The JavaScript code follows:
(async ()=>{
'use strict';
let secret = "sec-demo"; // the secret key
let enc = new TextEncoder("utf-8");
let body = "GET\npub-demo\n/v2/auth/grant/sub-key/sub-demo\nauth=myAuthKey&g=1&target-uuid=user-1×tamp=1595619509&ttl=300";
let algorithm = { name: "HMAC", hash: "SHA-256" };
let key = await crypto.subtle.importKey("raw", enc.encode(secret), algorithm, false, ["sign", "verify"]);
let signature = await crypto.subtle.sign(algorithm.name, key, enc.encode(body));
let digest = btoa(String.fromCharCode(...new Uint8Array(signature)));
console.log(digest);
})();
The original answer on this page was helpful in a debugging effort earlier today. We're using it to help identify a bug in our documentation for creating signatures for granting access tokens to use APIs with read/write permissions.
Somehow #StephenBlum's answer doesn't work for me.
I rewrite #StepanSnigirev' answer as async below instead.
"use strict";
(async () => {
const secret = "mysecretkey";
const enc = new TextEncoder();
const body = "myawesomedata";
const algorithm = { name: "HMAC", hash: "SHA-512" };
const key = await crypto.subtle.importKey(
"raw",
enc.encode(secret),
algorithm,
false,
["sign", "verify"]
);
const signature = await crypto.subtle.sign(
algorithm.name,
key,
enc.encode(body)
);
// convert buffer to byte array
const hashArray = Array.from(new Uint8Array(signature));
// convert bytes to hex string
const digest = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(digest);
})();
Related
I'm trying to exchange public keys between Browser and Server, and generate secret to be used for encryption of data. I'm trying to utilize ECDH (Elliptic Curve Diffie-Hellman).
On the Server side I'm generating ECDH with prime256v1 algorithm.
On the Browser side I'm generating ECDH with P-256 named curve. (these algorithms should be the same, they are just named differently, P-256 , also known as secp256r1 and prime256v1).
I'm able to pass Browser generated public key to the server as Base64 formatted string, and to generate secret using Server private key and Browser public key. And everything works fine on the Server side (import, generate secret, encryption).
But when I try to pass Server generated public key to the Browser as Base64 formatted string and try to import it, I get DOMException: Cannot create a key using the specified key usages.
const b64ToBin = (b64) => {
const binaryString = window.atob(b64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
const importB64Key = async (base64key) => {
const bin = b64ToBin(base64key);
console.log('bin ', bin);
const key = await window.crypto.subtle.importKey(
'raw',
bin,
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
['deriveKey']
);
return key;
};
The keyUsages value is wrong (as also pointed out by the error message): When importing a public key in the context of ECDH, an empty array [] is to be passed for keyUsages (deriveKey and/or deriveBits are only passed when importing a private key).
Also, keep in mind that the public EC key must be passed as an uncompressed or compressed key when using the format raw.
If the keyUsages value is fixed and an uncompressed or compressed key is applied, the posted code works.
(async () => {
var uncomressedKeyB64 = 'BAmL07vrRR5lfkWuH1RAFJufx0B4J+BdOqIYZCH+fJc8c+5sFch8aXEJ6qVgTnnYjKwrQ1BO3Tg28/F1h/FjMVQ=';
var comressedKeyB64 = 'AgmL07vrRR5lfkWuH1RAFJufx0B4J+BdOqIYZCH+fJc8';
const b64ToBin = (b64) => {
const binaryString = window.atob(b64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
const importB64Key = async (base64key) => {
const bin = b64ToBin(base64key);
console.log('bin ', bin);
const key = await window.crypto.subtle.importKey(
'raw',
bin,
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
[] // Fix: When importing a public key, an empty array must be passed for the key usages...
);
return key;
};
console.log(await importB64Key(uncomressedKeyB64));
console.log(await importB64Key(comressedKeyB64));
})();
I need to get JWT with EdDSA algorithm to be able to use an API. I have the private key to sign the message and I could do that with PHP with the next library: https://github.com/firebase/php-jwt (you can see the example with EdDSA at README). Now I need to do the same in JS but I didn't find the way to get JWT with a given secret key (encoded base 64) like that (only an example is not the real secretKey):
const secretKey = Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==
I tried a lot of libraries like jose, js-nacl, crypto, libsodium, etc. And I am really close to get the JWT with libsodium library, now I attach the code:
const base64url = require("base64url");
const _sodium = require("libsodium-wrappers");
const moment = require("moment");
const getJWT = async () => {
await _sodium.ready;
const sodium = _sodium;
const privateKey =
"Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const { msg, keyAscii} = encode(payload, privateKey, "EdDSA");
const signature = sodium.crypto_sign_detached(msg, keyDecoded); //returns Uint8Array(64)
//Here is the problem.
};
const encode = (payload, key, alg) => {
const header = {
typ: "JWT",
alg //'EdDSA'
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const headerAndPayloadBase64URL = `${headerBase64URL}.${payloadBase64URL}`;
const keyAscii= Buffer.from(key, "base64").toString("ascii");
return {headerAndPayloadBase64URL , keyAscii}
};
The problem is in the sodium.crypto_sign_detached function because it returns an Uint8Array(64) signature and and I need the JWT like that:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
How can I change the Uint8Array(64) to get the signature in a right format to get the JWT? I tried with base64, base64url, hex, text, ascii, etc and the final JWT is not valid (because the signature is wrong).
If you compare my code with the code that I mentioned with PHP is very similar but the function sodium.crypto_sign_detached returns Uint8Array(64) at JS library and the same function in PHP returns an string and I can get the token.
Or maybe there a way to adapt my given private key for use in other library (like crypto or jose where I received an error for the private key format)
Thank you!
In the posted NodeJS code there are the following issues:
crypto_sign_detached() returns the signature as a Uint8Array, which can be imported with Buffer.from() and converted to a Base64 string with base64url().
Concatenating headerAndPayloadBase64URL and the Base64url encoded signature with a . as separator gives the JWT you are looking for.
The raw private key must not be decoded with 'ascii', as this generally corrupts the data. Instead, it should simply be handled as buffer. Note: If for some reason a conversion to a string is required, use 'binary' as encoding, which produces a byte string (however, this is not an option with crypto_sign_detached() as this function expects a buffer).
With these changes, the following NodeJS code results:
const _sodium = require('libsodium-wrappers');
const base64url = require("base64url");
const getJWT = async () => {
await _sodium.ready;
const sodium = _sodium;
const privateKey = "Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const {headerAndPayloadBase64URL, keyBuf} = encode(payload, privateKey, "EdDSA");
const signature = sodium.crypto_sign_detached(headerAndPayloadBase64URL, keyBuf);
const signatureBase64url = base64url(Buffer.from(signature));
console.log(`${headerAndPayloadBase64URL}.${signatureBase64url}`) // eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
};
const encode = (payload, key, alg) => {
const header = {
typ: "JWT",
alg //'EdDSA'
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const headerAndPayloadBase64URL = `${headerBase64URL}.${payloadBase64URL}`;
const keyBuf = Buffer.from(key, "base64");
return {headerAndPayloadBase64URL, keyBuf};
};
getJWT();
Test:
Since Ed25519 is deterministic, the NodeJS code can be checked by comparing both JWTs: If, as in the above NodeJS code, the same header and payload are used as in the PHP code, the same signature and thus the same JWT is generated as by the PHP code, namely:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
which shows that the NodeJS code works.
Note that instead of the moment package, Date.now() could be used. This will return the time in milliseconds, so the value has to be divided by 1000, e.g. Math.round(Date.now()/1000), but saves a dependency.
What I'm trying to achieve
Sign a PDF in the browser using cliets certificate store or Smart Card
What I did so far
For accessing the local cert store I use FortifyApp.
Pdf is pre-signed on the server using iText(Sharp), then sent to the client via Ajax.
Relevant code:
using (var fileStream = new MemoryStream())
{
using (var stamper = PdfStamper.CreateSignature(reader, fileStream, '0', null, true))
{
var signatureAppearance = stamper.SignatureAppearance;
signatureAppearance.SetVisibleSignature(new iTextSharp.text.Rectangle(15,15,15,15), 1, "A");
IExternalSignatureContainer external =
new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
signatureAppearance.Reason = "AsdAsd";
signatureAppearance.Layer2Text = "Asd";
signatureAppearance.SignatureRenderingMode =
iTextSharp.text.pdf.PdfSignatureAppearance.RenderingMode.DESCRIPTION;
MakeSignature.SignExternalContainer(signatureAppearance, external, 512);
return fileStream.ToArray();
}
}
Following this, I managed to manipulate the pdf, extract byteRange, insert signature, etc. Relevant code:
let pdfBuffer = Buffer.from(new Uint8Array(pdf));
const byteRangeString = `/ByteRange `;
const byteRangePos = pdfBuffer.indexOf(byteRangeString);
if (byteRangePos === -1)
throw new Error('asd');
let len = pdfBuffer.slice(byteRangePos).indexOf(`]`) + 1;
// Calculate the actual ByteRange that needs to replace the placeholder.
const byteRangeEnd = byteRangePos + len;
const contentsTagPos = pdfBuffer.indexOf('/Contents ', byteRangeEnd);
const placeholderPos = pdfBuffer.indexOf('<', contentsTagPos);
const placeholderEnd = pdfBuffer.indexOf('>', placeholderPos);
const placeholderLengthWithBrackets = placeholderEnd + 1 - placeholderPos;
const placeholderLength = placeholderLengthWithBrackets - 2;
const byteRange = [0, 0, 0, 0];
byteRange[1] = placeholderPos;
byteRange[2] = byteRange[1] + placeholderLengthWithBrackets;
byteRange[3] = pdfBuffer.length - byteRange[2];
let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`;
actualByteRange += ' '.repeat(len - actualByteRange.length);
// Replace the /ByteRange placeholder with the actual ByteRange
pdfBuffer = Buffer.concat([pdfBuffer.slice(0, byteRangePos) as any, Buffer.from(actualByteRange), pdfBuffer.slice(byteRangeEnd)]);
// Remove the placeholder signature
pdfBuffer = Buffer.concat([pdfBuffer.slice(0, byteRange[1]) as any, pdfBuffer.slice(byteRange[2], byteRange[2] + byteRange[3])]);
and
//stringSignature comes from the signature creations below, and is 'hex' encoded
// Pad the signature with zeroes so the it is the same length as the placeholder
stringSignature += Buffer
.from(String.fromCharCode(0).repeat((placeholderLength / 2) - len))
.toString('hex');
// Place it in the document.
pdfBuffer = Buffer.concat([
pdfBuffer.slice(0, byteRange[1]) as any,
Buffer.from(`<${stringSignature}>`),
pdfBuffer.slice(byteRange[1])
]);
The problem
This uses forge, and an uploaded p12 file. - This would probably work, if I could translate the imported(?) privateKey from Fortify (which is === typeof CryptoKey, and forge throws an error: TypeError: signer.key.sign is not a function).
p7.addCertificate(certificate); //certificate is the Certificate from Fortify CertificateStore.getItem(certId)
p7.addSigner({
key: privateKey, //this is the CryptoKey from Fortify
certificate: null/*certificate*/, //also tried certificate from Fortify
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
}, {
type: forge.pki.oids.messageDigest,
// value will be auto-populated at signing time
}, {
type: forge.pki.oids.signingTime,
// value can also be auto-populated at signing time
// We may also support passing this as an option to sign().
// Would be useful to match the creation time of the document for example.
value: new Date(),
},
],
});
// Sign in detached mode.
p7.sign({detached: true});
I also tried pkijs for creating the signature (throws a similar error: Signing error: TypeError: Failed to execute 'sign' on 'SubtleCrypto': parameter 2 is not of type 'CryptoKey'.)
let cmsSigned = new pki.SignedData({
encapContentInfo: new pki.EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1", // "data" content type
eContent: new asn.OctetString({ valueHex: pdfBuffer })
}),
signerInfos: [
new pki.SignerInfo({
sid: new pki.IssuerAndSerialNumber({
issuer: certificate.issuer,
serialNumber: certificate.serialNumber
})
})
],
certificates: [certificate]
});
let signature = await cmsSigned.sign(privateKey, 0, 'SHA-256');
What "works" is, if I create the signature using the code below:
let signature = await provider.subtle.sign(alg, privateKey, new Uint8Array(pdfBuffer).buffer);
"works", because it creates an invalid signature:
Error during signature verification.
ASN.1 parsing error:
Error encountered while BER decoding:
I tried multiple certificates, no luck.
Questions
Can I achieve my goal without having to manually upload a p12/pfx file, is it even possible?
Is the server-side implementation of the deferred signature correct, do I need something else?
Is the pdf manipulation in javascript correct?
Can I transform the native CrytpoKey to forge or pkijs?
What is wrong with the last signature? At first glance it seems right (at least the format):
<>>>/ContactInfo()/M(D:20200619143454+02'00')/Filter/Adobe.PPKLite/SubFilter/adbe.pkcs7.detached/ByteRange [0 180165 181191 1492] /Contents <72eb2731c9de4a5ccc94f1e1f2d9b07be0c6eed8144cb73f3dfe2764595dcc8f58b8a55f5026618fd9c79146ea93afdafc00b617c6e70de553600e4520f290bef70c499ea91862bb3acc651b6a7b162c984987f05ec59db5b032af0127a1224cad82e3be38ae74dd110ef5f870f0a0a92a8fba295009f267508c372db680b3d89d3157d3b218f33e7bf30c500d599b977c956e6a6e4b02a0bbd4a86737378b421ae2af0a4a3c03584eaf076c1cdb56d372617da06729ef364605ecd98b6b32d3bb792b4541887b59b686b41db3fc32eb4c651060bb02e2babeb30e6545834b2935993f6ee9edcc8f99fee8ad6edd2958c780177df6071fdc75208f76bbbcc21a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000>>>
Thanks:
F
Original answer:
So I figured it out.
Can I achieve my goal without having to manually upload a p12/pfx
file, is it even possible?
Yes, it is. (See below on what needs to be changed.)
Is the server-side implementation of the deferred signature correct, do I need something else?
Yes, the code above is fine.
Is the pdf manipulation in javascript correct?
Also fine.
Can I transform the native CrytpoKey to forge or pkijs?
Yes, see below.
What is wrong with the last signature?
#mkl answered it in a comment, thank you.
FortifyApp has a CMS demo now. Although it didn't work with the version I was using, it works with version 1.3.4.
So I went with the pki.js implementation. The code changes need for the signing to be successful are the following:
Export the certificate:
const cryptoCert = await provider.certStorage.getItem(selectedCertificateId);
const certRawData = await provider.certStorage.exportCert('raw', cryptoCert);
const pkiCert = new pki.Certificate({
schema: asn.fromBER(certRawData).result,
});
return pkiCert;
Sign in detached mode
let cmsSigned = new pki.SignedData({
version: 1,
encapContentInfo: new pki.EncapsulatedContentInfo({
eContentType: '1.2.840.113549.1.7.1',
}),
signerInfos: [
new pki.SignerInfo({
version: 1,
sid: new pki.IssuerAndSerialNumber({
issuer: certificate.issuer,
serialNumber: certificate.serialNumber
})
})
],
certificates: [certificate]
});
let signature = await cmsSigned.sign(privateKey, 0, 'SHA-256', pdfBuffer);
const cms = new pki.ContentInfo({
contentType: '1.2.840.113549.1.7.2',
content: cmsSigned.toSchema(true),
});
const result = cms.toSchema().toBER(false);
return result;
Convert signature to 'HEX' string
let stringSignature = Array.prototype.map.call(new Uint8Array(signature), x => (`00${x.toString(16)}`).slice(-2)).join('');
let len = signature.byteLength;
Update (summary on the js side of things):
Download the pre-signed pdf (+ byteRange - this can be extracted with iText, so you can apply multiple signatures)
Prepare the signature (see first part of point 3. in the question)
Get private key:
const provider = await this.ws.getCrypto(selectedProviderId); // this.ws is a WebcryptoSocket
provider.sign = provider.subtle.sign.bind(provider.subtle);
setEngine(
'newEngine',
provider,
new CryptoEngine({
name: '',
crypto: provider,
subtle: provider.subtle,
})
);
const key = await this.getCertificateKey('private', provider, selectedCertificateId); //can be null
See Original answer points 1. and 2. Between theese I also have a hack:
let logout = await provider.logout();
let loggedIn = await provider.isLoggedIn();
if (!loggedIn) {
let login = await provider.login();
}
Add the signature on the pdf. Use original answer point 3., then the second part of point 3 in the question.
How to get to "feature parity" between Node's Crypto.createHmac( 'sha256', buffer) and CryptoJS.HmacSHA256(..., secret)?
I have a 3rd party code that does what you can see here as the method node1. I would need to achieve the same result in the browser. Seemingly, the difference is in the that the secret is base64 decoded on the node side. But I still can't get the same output.
const CryptoJS = require('crypto-js')
const Crypto = require('crypto')
const message = "Message"
const secret = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
function node1() {
return Crypto.createHmac("sha256", Buffer.from(secret, 'base64'))
.update(message, "utf8")
.digest("base64");
}
function node2() {
return Crypto.createHmac("sha256", Buffer.from(secret, 'base64').toString('base64'))
.update(message, "utf8")
.digest("base64");
}
function browser() {
const crypted = CryptoJS.HmacSHA256(message, secret)
return CryptoJS.enc.Base64.stringify(crypted)
}
console.log('node1', node1())
console.log('node2', node2())
console.log('browser-like', browser())
// node1 agitai8frSJpJuXwd4HMJC/t2tluUJPMZy8CeYsEHTE=
// node2 fxJQFWs5W3A4otaAlnlV0kh4yfQPb4Y1ChSVZsUAAXA=
// browser-like fxJQFWs5W3A4otaAlnlV0kh4yfQPb4Y1ChSVZsUAAXA=
So, I can reproduce a naive browser-like behaviour in node. This gave me the idea to use atob in the browser, to reproduce node's behaviour. The following sign method is my best guess on the browser side.
function sign(message) {
const crypted = CryptoJS.HmacSHA256(message, atob(secret));
return CryptoJS.enc.Base64.stringify(crypted)
}
function signNotDecoded(message) {
const crypted = CryptoJS.HmacSHA256(message, secret);
return CryptoJS.enc.Base64.stringify(crypted)
}
console.log('browser', sign('Message'))
console.log('browser-like', signNotDecoded('Message'))
// browser dnVm5jBgIBNV6pFd4J9BJTjx3BFsm7K32SCcEQX7RHA=
// browser-like fxJQFWs5W3A4otaAlnlV0kh4yfQPb4Y1ChSVZsUAAXA=
So, running signDecoded() in the browser, and running browser() in node gives the same output. Running both node2() and browser() in node again provide the same output, but still sign() differs from node1().
Based on the above, I'm quite sure that the problem is with my usage of atob, but what do I miss there?
Change
atob(secret)
To
CryptoJS.enc.Base64.parse(secret)
Because if you pass a raw string as key to the function it will be re-parsed as UTF-8.
In this question Erik needs to generate a secure random token in Node.js. There's the method crypto.randomBytes that generates a random Buffer. However, the base64 encoding in node is not url-safe, it includes / and + instead of - and _. Therefore, the easiest way to generate such token I've found is
require('crypto').randomBytes(48, function(ex, buf) {
token = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
});
Is there a more elegant way?
Try crypto.randomBytes():
require('crypto').randomBytes(48, function(err, buffer) {
var token = buffer.toString('hex');
});
The 'hex' encoding works in node v0.6.x or newer.
Synchronous option in-case if you are not a JS expert like me. Had to spend some time on how to access the inline function variable
var token = crypto.randomBytes(64).toString('hex');
1. Using nanoid third party library [NEW!]
A tiny, secure, URL-friendly, unique string ID generator for JavaScript
https://github.com/ai/nanoid
import { nanoid } from "nanoid";
const id = nanoid(48);
2. Base 64 Encoding with URL and Filename Safe Alphabet
Page 7 of RCF 4648 describes how to encode in base 64 with URL safety.
You can use an existing library like base64url to do the job.
The function will be:
var crypto = require('crypto');
var base64url = require('base64url');
/** Sync */
function randomStringAsBase64Url(size) {
return base64url(crypto.randomBytes(size));
}
Usage example:
randomStringAsBase64Url(20);
// Returns 'AXSGpLVjne_f7w5Xg-fWdoBwbfs' which is 27 characters length.
Note that the returned string length will not match with the size argument (size != final length).
3. Crypto random values from limited set of characters
Beware that with this solution the generated random string is not uniformly distributed.
You can also build a strong random string from a limited set of characters like that:
var crypto = require('crypto');
/** Sync */
function randomString(length, chars) {
if (!chars) {
throw new Error('Argument \'chars\' is undefined');
}
const charsLength = chars.length;
if (charsLength > 256) {
throw new Error('Argument \'chars\' should not have more than 256 characters'
+ ', otherwise unpredictability will be broken');
}
const randomBytes = crypto.randomBytes(length);
let result = new Array(length);
let cursor = 0;
for (let i = 0; i < length; i++) {
cursor += randomBytes[i];
result[i] = chars[cursor % charsLength];
}
return result.join('');
}
/** Sync */
function randomAsciiString(length) {
return randomString(length,
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}
Usage example:
randomAsciiString(20);
// Returns 'rmRptK5niTSey7NlDk5y' which is 20 characters length.
randomString(20, 'ABCDEFG');
// Returns 'CCBAAGDGBBEGBDBECDCE' which is 20 characters length.
The up-to-date right way to do this asynchronously using ES 2016 standards of async and await (as of Node 7) would be the following:
const crypto = require('crypto');
function generateToken({ stringBase = 'base64', byteLength = 48 } = {}) {
return new Promise((resolve, reject) => {
crypto.randomBytes(byteLength, (err, buffer) => {
if (err) {
reject(err);
} else {
resolve(buffer.toString(stringBase));
}
});
});
}
async function handler(req, res) {
// default token length
const newToken = await generateToken();
console.log('newToken', newToken);
// pass in parameters - adjust byte length
const shortToken = await generateToken({byteLength: 20});
console.log('newToken', shortToken);
}
This works out of the box in Node 7 without any Babel transformations
Random URL and filename string safe (1 liner)
Crypto.randomBytes(48).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
As of Node.js 14.18 and 15.7, url-safe base64 encoding support is built-in:
const token = crypto.randomBytes(48).toString('base64url');
If you want to use the async version (because the function may have to wait for entropy), it can be promisified to align better with modern patterns:
const randomBytesAsync = util.promisify(crypto.randomBytes);
const token = (await randomBytesAsync(48)).toString('base64url');
With async/await and promisification.
const crypto = require('crypto')
const randomBytes = Util.promisify(crypto.randomBytes)
const plain = (await randomBytes(24)).toString('base64').replace(/\W/g, '')
Generates something similar to VjocVHdFiz5vGHnlnwqJKN0NdeHcz8eM
Check out:
var crypto = require('crypto');
crypto.randomBytes(Math.ceil(length/2)).toString('hex').slice(0,length);
crypto-random-string is a nice module for this.
const cryptoRandomString = require('crypto-random-string');
cryptoRandomString({length: 10}); // => '2cf05d94db'
cryptoRandomString({length: 10, type: 'base64'}); // => 'YMiMbaQl6I'
cryptoRandomString({length: 10, type: 'url-safe'}); // => 'YN-tqc8pOw'
cryptoRandomString({length: 10, type: 'numeric'}); // => '8314659141'
cryptoRandomString({length: 6, type: 'distinguishable'}); // => 'CDEHKM'
cryptoRandomString({length: 10, type: 'ascii-printable'}); // => '`#Rt8$IK>B'
cryptoRandomString({length: 10, type: 'alphanumeric'}); // => 'DMuKL8YtE7'
cryptoRandomString({length: 10, characters: 'abc'}); // => 'abaaccabac'
cryptoRandomString.async(options) add .async if you want to get a promise.
Look at real_ates ES2016 way, it's more correct.
ECMAScript 2016 (ES7) way
import crypto from 'crypto';
function spawnTokenBuf() {
return function(callback) {
crypto.randomBytes(48, callback);
};
}
async function() {
console.log((await spawnTokenBuf()).toString('base64'));
};
Generator/Yield Way
var crypto = require('crypto');
var co = require('co');
function spawnTokenBuf() {
return function(callback) {
crypto.randomBytes(48, callback);
};
}
co(function* () {
console.log((yield spawnTokenBuf()).toString('base64'));
});
https://www.npmjs.com/package/crypto-extra has a method for it :)
var value = crypto.random(/* desired length */)
The npm module anyid provides flexible API to generate various kinds of string ID / code.
To generate random string in A-Za-z0-9 using 48 random bytes:
const id = anyid().encode('Aa0').bits(48 * 8).random().id();
// G4NtiI9OYbSgVl3EAkkoxHKyxBAWzcTI7aH13yIUNggIaNqPQoSS7SpcalIqX0qGZ
To generate fixed length alphabet only string filled by random bytes:
const id = anyid().encode('Aa').length(20).random().id();
// qgQBBtDwGMuFHXeoVLpt
Internally it uses crypto.randomBytes() to generate random.
Simple function that gets you a token that is URL safe and has base64 encoding! It's a combination of 2 answers from above.
const randomToken = () => {
crypto.randomBytes(64).toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
}
in your terminal just write
node -e "console.log(crypto.randomBytes(48).toString('hex'))"
Or in your code use:
const randomToken = () => {
crypto.randomBytes(48).toString('hex');
}
0 dependency free solution... Works in browsers, deno & nodejs (with new global web crypto)
const random = size => btoa(
String.fromCharCode(
...crypto.getRandomValues(
new Uint8Array(size)
)
)
).replaceAll('+', 'x').replaceAll('/', 'I').slice(0, size)
for (let i = 5; i--;) console.log(random(16))
All doe I would just have used one single uint8array \w predefined length and called crypto.getRandomValues whenever I need something uniq (and slice it if i have to) and never deal with strings or base64, base64 is just unnecessary overhead.
(allocating lots of buffer to fast can be costly)
const buf256 = new Uint8Array(256)
const random = crypto.getRandomValues.bind(crypto, buf256)
for (let i = 5; i--;) random()//.slice()
You can use the random-token lib. it's very easy to use . :)
var randomToken = require('random-token').create('abcdefghijklmnopqrstuvwxzyABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
var token = randomToken(16);
And also you can not use different salt
var randomToken = require('random-token');
var token = randomToken(16); // output -> d8d4kd29c40f021 ```