In my logic i am hashing some data based on a secret key. Later I want to verify that signature. I am using the crypto package in nodejs. Specifically, in the verifier.verify function, the docs require a publicKey. How would i go upon doing this as i am using the secret in config?
Any help would be awesome!
let data = {
"data": 15
}
config: {
secret: 'mgfwoieWCVBVEW42562tGVWS',
}
let stringData = JSON.stringify(data)
const hash = crypto.createHmac('sha256', config.secret)
.update(stringData, 'utf-8')
.digest('base64')
const verifier = crypto.createVerify('sha256')
let ver = verifier.verify(publicKey, stringData, 'base64')
console.log(ver);
If you want to verify a particular signature in node you can use the following
config: {
secret: "IloveMyself"
};
var cipherPassword = crypto.createCipher('aes-256-ctr', config.secret);
var dbpassword = cipherPassword.update(dbpassword, 'utf-8', 'hex');
This will be for creating encryption of the password. Now to verify the password/signature again in NodeJS, you need to do the following:
var cipher = crypto.createDecipher('aes-256-ctr', config.secret);
var dbPassword = cipher.update(dbpassword, 'hex', 'utf-8');
This will give decrypted password/signature which you can compare easily. I hope it helps!
Related
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.
I have a client in JavaScript and a server in Node.JS. I'm trying to sign a simple text in client and send the signature along with publicKey to the server then server can verify the publicKey.
Anything in client-side is OK! but I'm unable to verify the signature in server-side. I think there is no need for you to read the client code but just for assurance I'll provide it too.
Client code:
let privateKey = 0;
let publicKey = 0;
let encoded = '';
let signatureAsBase64 = '';
let pemExported = ''
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
let keygen = crypto.subtle.generateKey({
name: 'RSA-PSS',
modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]),
hash: 'SHA-256'
}, true, ['sign', 'verify']);
keygen.then((value)=>{
publicKey = value.publicKey;
privateKey = value.privateKey;
let exported = crypto.subtle.exportKey('spki', publicKey);
return exported
}).then((value)=>{
console.log('successful');
const exportedAsString = ab2str(value);
const exportedAsBase64 = btoa(exportedAsString);
pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;
//signing:
encoded = new TextEncoder().encode('test');
let signing = crypto.subtle.sign({
name: "RSA-PSS",
saltLength: 32
},
privateKey,
encoded);
return signing;
}).then((signature)=>{
const signatureAsString = ab2str(signature);
signatureAsBase64 = btoa(signatureAsString);
//verifying just to be sure everything is OK:
return crypto.subtle.verify({
name: 'RSA-PSS',
saltLength: 32
},
publicKey,
signature,
encoded)
}).then((result)=>{
console.log(result);
//send information to server:
let toSend = new XMLHttpRequest();
toSend.onreadystatechange = ()=>{
console.log(this.status);
};
toSend.open("POST", "http://127.0.0.1:3000/authentication", true);
let data = {
signature: signatureAsBase64,
publicKey: pemExported
};
toSend.setRequestHeader('Content-Type', 'application/json');
toSend.send(JSON.stringify(data));
//to let you see the values, I'll print them to console in result:
console.log("signature is:\n", signatureAsBase64);
console.log("publicKey is:\n", pemExported);
}).catch((error)=>{
console.log("error",error.message);
})
Server Code(I use express for this purpose):
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
router.post('/authentication', async (req, res)=>{
try{
const publicKey = crypto.createPublicKey({
key: req.body.publicKey,
format: 'pem',
type: 'spki'
});
console.log(publicKey.asymmetricKeyType, publicKey.asymmetricKeySize, publicKey.type);
let signature = Buffer.from(req.body.signature, 'base64').toString();
signature = str2ab(signature);
const result = crypto.verify('rsa-sha256', new TextEncoder().encode('test'),
publicKey, new Uint8Array(signature));
console.log(result);
}catch(error){
console.log('Error when autheticating user: ', error.message);
}
})
Server Console Log:
rsa undefined public
false
NOTE:
I think the public key is imported correctly in server because when I export the
public key again in server, the pem formats of both sides(client & server) are completely
equal. so I think the problem is associated with 'verification' or 'converting signature' in server.
I prefer to use the built-in crypto module if it's possible, so other libraries such as subtle-crypto are my second options and I'm here to see if this can be done with crypto or not.
I want to learn how to verify a signature that is signed by JavaScript SubtleCrypto, due to this, Please don't ask some questions such as:
Why do you want to verify the public key in server?
Why don't you use 'X' library in client?
Feel free to change Exported format(pem), Public key format('spki'), Algorithm format(RSA-PSS) and so on.
The failed verification has two reasons:
The PSS padding must be specified explicitly, since PKCS#1 v1.5 padding is the default, s. here.
The conversion of the signature corrupts the data: The line:
let signature = Buffer.from(req.body.signature, 'base64').toString();
performs a UTF8 decoding, s. here, which irreversibly changes the data, s. here. The signature consists of binary data that is generally UTF8 incompatible. A conversion to a string is only possible with suitable binary-to-text encodings (like Base64, hex etc.), s. here.But apart from that a conversion is actually not necessary at all, because the signature can be passed directly as a buffer, s. here.
The following NodeJS code performs a successful verification (for a signature and public key produced with the client code):
const publicKey = crypto.createPublicKey(
{
key: req.body.publicKey,
format: 'pem',
type: 'spki'
});
const result = crypto.verify(
'rsa-sha256',
new TextEncoder().encode('test'),
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
},
Buffer.from(req.body.signature, 'base64'));
console.log(result); // true
Based On The AWS in Browser-Based Uploads Using POST Docs, I'm attempting to upload audio files to my bucket without having to pull in an entire SDK. I'm using Vue.js. When I make a request, here is the error I get back:
<Error>
<Code>InvalidRequest</Code>
<Message>The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.</Message>
<RequestId>7FE397A138CF89</RequestId>
<HostId>rEx4jk6vj363wlVGrGqutyDkMNeUhi6DizAXhAiIWrIpG8Rups1rLFGO4Dge5loeNj</HostId>
</Error>
CODE TO CREATE A POLICY, SIGNING KEY & SIGNATURE
Policy Func - creates JSON obj of conditions & returns Utf-8 & Base64 encoded version (as per AWS Doc's)
getPolicy (date) {
let moment = this.$moment(date) // using moment.js
let formattedDate = moment.format('YYYYMMDD')
let obj = {
"expiration": date,
"conditions": [
{"bucket": "test-bucket"},
{"acl": "public-read"},
{"key": "test.mp3"},
["starts-with", "$Content-Type", "audio/"],
["content-length-range", 1048579, 3000000000],
{"x-amz-server-side-encryption": "AES256"},
{"x-amz-credential": `KJIAI4OQHKZGIBSQY5TQ/${formattedDate}/us-east-2/s3/aws4_request`},
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
{"x-amz-date": formatedDate}
]
}
let string = JSON.stringify(obj)
let utf8 = encodeURI(string)
let base64 = btoa(utf8)
return base64
}
Signing Key Func - creates an HmacSHA256 Signing Key (as per AWS Doc's)
getSigningKey (date) {
// AWSSecretAccessKeyId (obviously this is a dummy)
let key = '+eo98jdkXTjOYO2weY84m2vzCV63vMI6yGvC097R'
let dateKey = crypto.HmacSHA256(date, `AWS4${key}`)
let dateRegionKey = crypto.HmacSHA256('us-east-2', dateKey)
let dateRegionServiceKey = crypto.HmacSHA256('s3', dateRegionKey)
let signingKey = crypto.HmacSHA256('aws4_request', dateRegionServiceKey)
return signingKey.toString()
}
Signature Func - creates a hex-encoded, HmacSHA256 signature (as per AWS Doc's)
getSignature (date) {
let policy = this.getPolicy(date)
let signingKey = this.getSigningKey(date)
let signature = crypto.HmacSHA256(policy, signingKey)
let hexEncodedSignature = signature.toString(hex)
return hexEncodedSignature
}
AJAX REQUEST MADE USING AXIOS
uploadFile (file) {
const date = new Date().toISOString()
let moment = this.$moment(date)
let formattedDate = b.format('YYYYMMDD')
const policy = this.getPolicy(date)
const signature = this.getSignature(date)
const form = new FormData()
form.append('key', 'test.mp3')
form.append('acl', 'public-read')
form.append('Content-Type', 'audio/*')
form.append('x-amz-server-side-encryption', 'AES256')
form.append('X-Amz-Credential', `KJIAI4OQHKZGIBSQY5TQ/${formattedDate}/us-east-2/s3/aws4_request`)
form.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256')
form.append('X-Amz-Date', formattedDate)
form.append('AWSAccessKeyId', 'KJIAI4OQHKZGIBSQY5TQ')
form.append('Policy', policy)
form.append('Signature', signature)
form.append('file', file)
return axios.post('https://test-bucket.s3.us-east-2.amazonaws.com/', form).then((response) => {
// do something
}
WHAT AM I DOING WRONG TO RECEIVE THE RESPONSE ERROR?
My code to get my Balance from the Kraken API does work in Python (based on the krakenex library), but not in the JS version (based loosely off the kraken-api library, but with the crypto library substituted for crypto-js). The error is always: Invalid Key.
Even when I copy the headers and the nonce sent by the Python client into Postman, I get Invalid Key.
I believe the signature and nonce to be valid, because when they are not, Kraken retorts that either the signature or nonce are invalid.
Is there anything else that Javascript's fetch does differently than Python3 requests? Because the body and headers are otherwise identical.
JS code that generates auth data:
const getMessageSignature = (path, request, secret, nonce) => {
// API-Sign = Message signature using HMAC-SHA512 of (URI path + SHA256(nonce + POST data)) and base64 decoded secret API key
const message = qs.stringify(request);
console.log(message);
const secret_buffer = btoa(secret);
const hash = CryptoJS.algo.SHA256.create();
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA512, secret_buffer);
const hash_digest = hash.update(nonce + message).finalize().toString(CryptoJS.enc.Hex);
const hmac_digest = hmac.update(path + hash_digest).finalize().toString(CryptoJS.enc.Base64);
// CANNOT USE ORIGINAL LIB CODE (Buffer, got and crypto not supported)
// const secret_buffer = new Buffer(secret, 'base64');
// const hash = new crypto.createHash('sha256');
// const hmac = new crypto.createHmac('sha512', secret_buffer);
// const hash_digest = hash.update(nonce + message).digest('binary');
// const hmac_digest = hmac.update(path + hash_digest, 'binary').digest('base64');
return hmac_digest;
};
Update:
In fact, the following observations are weird:
correct key + correct signature = "incorrect key"
incorrect key + correct signature = "incorrect key"
incorrect key + incorrect signature = "incorrect key"
correct key + incorrect signature = "invalid signature"
what gives?
Update2
Seems the requests are identical (other than the signature and nonce of course, which will and should change with every request).
Turns out it was the signature after all and Kraken simply doesn't give very accurate responses (which makes some sense, but is a pain if you're trying to figure something out). Finally I was able to rewrite the code using CryptoJS only:
const getMessageSignature = (path, request, secret, nonce) => {
// API-Sign = Message signature using HMAC-SHA512 of (URI path + SHA256(nonce + POST data)) and base64 decoded secret API key
const message = JSON.stringify(request);
const hash = CryptoJS.SHA256(nonce + message);
const secret_buffer = CryptoJS.enc.Base64.parse(secret);
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA512, secret_buffer);
hmac.update(path, secret_buffer);
hmac.update(hash, secret_buffer);
return hmac.finalize().toString(CryptoJS.enc.Base64);
};
This yields a correct signature and Kraken no longer complains. Zzah.
I'm building a Facebook Page app in Classic ASP. I've been unable to match the signature that Facebook passes into the app as the first part of the POSTed signed_request.
Because there are few libraries for cryptography in VBScript, I'm using server side Javascript and the crypto-js library from https://code.google.com/archive/p/crypto-js/
I've tried to translate the PHP code example from Facebook's docs at https://developers.facebook.com/docs/games/gamesonfacebook/login#parsingsr into Javascript. I can generate an HMAC SHA256 hash of the signed_request payload but that doesn't match the signed_request signature.
I think the problem is that Facebook's signature is in a different format. It looks to be binary (~1抚Ö.....) while the HMAC SHA256 hash I'm generating is a hexadecimal string (7f7e8f5f.....). In Facebook's PHP example the hash_hmac function uses the raw binary parameter. So I think I need to either convert Facebook's signature to hexadecimal or my signature to binary in order to do an "apples-to-apples" comparison and get a match.
Here's my code:
/* Use the libraries from https://code.google.com/archive/p/crypto-js/
crypto-js/crypto-js.min.js
crypto-js/hmac-sha256.min.js
crypto-js/enc-base64.min.js
*/
var signedRequest = Request.queryString("signed_request")
var FB_APP_SECRET = "459f038.....";
var arSR = signedRequest.split(".");
var encodedSig = arSR[0];
var encodedPayload = arSR[1];
var payload = base64UrlDecode(encodedPayload);
var sig = base64UrlDecode(encodedSig);
var expectedSig;
expectedSig = CryptoJS.HmacSHA256(encodedPayload, FB_APP_SECRET); // Unaltered payload string; no match
expectedSig = CryptoJS.HmacSHA256(payload, FB_APP_SECRET); // base64-decoded payload string; no match
if (sig == expectedSig) {
Response.write(payload);
} else {
Response.write("Bad signature");
}
function base64UrlDecode(input) {
// Replace characters and convert from base64.
return Base64.decode(input.replace("-", "+").replace("_", "/"));
}
After looking into the crypto-js documentation about encoding I found the solution. The de-/encoding methods provided by crypto-js are listed under 'Encoders' at the bottom of https://code.google.com/archive/p/crypto-js/ (Thanks for the nudge, CBroe.)
The solution was to use .toString() on the signatures. It seems like crypto-js uses a word format that was preventing a comparison match. I did also switch to using the base64 decoding provided by crypto-js in order to stick with one library.
Here's my updated code:
/* Use the libraries from https://code.google.com/archive/p/crypto-js/
crypto-js/crypto-js.min.js
crypto-js/hmac-sha256.min.js
crypto-js/enc-base64.min.js
*/
var signedRequest = Request.queryString("signed_request")
var FB_APP_SECRET = "459f038.....";
var arSR = signedRequest.split(".");
var encodedSig = arSR[0];
var encodedPayload = arSR[1];
var payload = base64UrlDecode(encodedPayload);
var sig = base64UrlDecode(encodedSig);
var expectedSig = CryptoJS.HmacSHA256(encodedPayload, FB_APP_SECRET); /******** Correct payload */
if (sig.toString() != expectedSig.toString()) { /******* Use .toString() to convert to normal strings */
Response.write(payload);
} else {
Response.write("Bad signature");
}
function base64UrlDecode(input) {
return CryptoJS.enc.Base64.parse( /******** Decode */
input.replace("-", "+").replace("_", "/") // Replace characters
);
}
I recently implemented this for their required user data deletion webhook. No external dependencies needed anymore:
const crypto = require('crypto');
function parseSignedRequest(signedRequest, secret) {
const [signatureReceived, encodedPayload] = signedRequest.split('.', 2);
const payload = b64decode(encodedPayload)
const data = JSON.parse(payload);
const hmac = crypto.createHmac('sha256', secret).update(payload);
const expectedSignature = hmac.digest('base64');
if (signatureReceived === expectedSignature) {
return data;
} else {
throw new Error("Signature mismatch");
}
}
function b64decode(data) {
const buff = Buffer.from(data, 'base64');
return buff.toString('ascii');
}
It's a translation of their example PHP code. I also have a repo setup with tests.
I found this worked for me.
const crypto = require('crypto')
const _atob = (str) => Buffer.from(str, 'base64').toString('binary')
const parseSignedRequest = (signed_request, app_secret) => {
const [encoded_sig, payload] = signed_request.split('.')
const json = _atob(payload)
const data = JSON.parse(json)
if (!data.algorithm || data.algorithm.toUpperCase() !== 'HMAC-SHA256') {
return {error: true, type: 'Unknown algorithm. Expected HMAC-SHA256'}
}
// check sig
const expected_sig = crypto.createHmac('sha256', config.facebook.app_secret)
.update(payload).digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_')
.replace(/=/g, '')
if (encoded_sig !== expected_sig) {
return ({error: true, type: 'invalid_signature'})
}
return {error: false, parsedRequest: data}
}
const {error, type, parsedRequest} = parseSignedRequest(signed_request)