How to verify Patreon's webhook message? - node.js

From Patreon documentation:
the message signature is the HEX digest of the message body HMAC signed (with MD5) using your webhook's secret viewable on the webhooks page. You can use this to verify us as the sender of the message.
This is how I have tried to verify the message in my Express server:
import express from 'express';
import CryptoJS from 'crypto-js';
const router = express();
router.post('/webhook', function (req, res) {
const secret = 'Secret from https://www.patreon.com/portal/registration/register-webhooks';
console.log(CryptoJS.HmacMD5(req.body, secret).toString(CryptoJS.enc.Hex))
console.log(CryptoJS.HmacMD5(JSON.stringify(req.body), secret).toString(CryptoJS.enc.Hex))
const wordArray = CryptoJS.enc.Utf8.parse(req.body)
const hexString = CryptoJS.enc.Hex.stringify(wordArray);
console.log(CryptoJS.HmacMD5(hexString, secret).toString(CryptoJS.enc.Hex))
res.send();
});
But all these results that I am logging are not the same compared to the X-Patreon-Signature value that I am getting from the header.

Make sure your secret is correct in your code first then go ahead and create a new file that uses crypto to hash payloads.
I started with making a hashing module file with the funtion ComputeHash. This function takes a secret and payload for arguments:
const crypto = require('crypto');
/**
* #return {string}
*/
exports.ComputeHash = function (secret, payload)
{
// string to be hashed
const str = JSON.stringify(payload);
// create a md5 hasher
const md5Hasher = crypto.createHmac("md5", secret);
// hash the string
// and set the output format
const hash = md5Hasher.update(str).digest("hex");
return(hash);
};
Now this is how it should be Implemented to work with patreon in your API file:
DO note: in order to verify, you have to compare the SIGNATURE and HASH variables. If those are equal, it's verified.
I would suggest to implement this with middleware, but for demonstational purposes I've done it like this.
const Hasher = require('./../modules/Hasher');
//more code
.post('/', async (req, res) => {
try {
const secret = config.token;
const signature = req.headers["x-patreon-signature"];
const Hash = Hasher.ComputeHash(secret, req.body);
console.log(signature);
console.log(Hash);
const verified = (signature === Hash);
} catch (e) {
res.status(401);
res.end();
}
// more code

Related

In cloudflare worker how to get data from url

How to get data params from url and decrypt using the function below in cloudflare worker.https://SOMEID.srv10.workers.dev/hash/ So i want to get hash and decrypt that using that function.
async function decryptData(encryptedData, password) {
try {
// split the IV off from the end (delimited using ::)
encryptedComponents= encryptedData.split("::")
const data = hexStringToUint8Array(encryptedComponents[0])
const iv = hexStringToUint8Array(encryptedComponents[1])
const passwordKey = await getPasswordKey(password, ['decrypt'])
const decryptedContent = await crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv: iv,
},
passwordKey,
data,
)
return arraybufferToString(decryptedContent)
} catch (e) {
throw e
}
}
Assuming your hash is a query parameter you can simply read it like so:
// convert the worker request url into a URL object
const url = new URL(request.url);
// read the encrypted data from the query param
const encryptedData = url.searchParams.get('hash');
// call your function
await decryptData(encryptedData, '<your-password-here>');

how to get public key from getSigningKey function of jwks-rsa

i have tried two library of nodejs "jwks-rsa" and "jwks-client" to get public key to authenticate Apple login. But its giving error in getPublicKey is not a function.
Then i console the key which is return by getSigningKey is giving me -1.
i don't know why this is behaving like that.
const header = token.split(".");
const jwtHead = Buffer.from(header[0], "base64").toString();
const kid = JSON.parse(jwtHead).kid;
const jwksClient = require('jwks-client');
const client = jwksClient({
strictSsl: true, // Default value
jwksUri: process.env.APPLE_URL
});
client.getSigningKey(kid, (err, key) => {
console.log(key)
const signingKey = key.publicKey;
console.log(signingKey);
});
For me it works as expected I've try it with your configuration:
const kid = '86D88Kf'; // specify kid explicitly
const client = jwksClient({
strictSsl: true,
jwksUri: 'https://appleid.apple.com/auth/keys'
});
client.getSigningKey(kid, (err, key) => {
console.log(key)
const signingKey = key.publicKey;
console.log(signingKey);
});
And I get a legit public key in the response:
Probably you have some misconfiguration.
The problem is with the find() method used inside getSigningKey(kid).find() method has compatibility issues find() compatibility issue. We can use filter() method instead.
Instead of using client.getSigningKey(kid) try using keys = client.getSigningKeys() then filter the response const key = keys.filter(k => k.kid === kid);

NodeJS Crypto Fails to Verify Signature Created by Web Crypto API

I'm having troubles verifying signatures created by the Web Crypto API.
Here is the code I'm using to generate RSA keys in the browser:
let keys;
const generateKeys = async () => {
const options = {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
};
keys = await window.crypto.subtle.generateKey(
options,
false, // non-exportable (public key still exportable)
['sign', 'verify'],
);
};
And to export the public key:
const exportPublicKey = async () => {
const publicKey = await window.crypto.subtle.exportKey('spki', keys.publicKey);
let body = window.btoa(String.fromCharCode(...new Uint8Array(publicKey)));
body = body.match(/.{1,64}/g).join('\n');
return `-----BEGIN PUBLIC KEY-----\n${body}\n-----END PUBLIC KEY-----`;
// Output:
//
// -----BEGIN PUBLIC KEY-----
// MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx7J3SUG4sq/HSGIaGZWY
// 8b26cfEpVFYHoDUDUORIJzA/fLE9aj+uOKpGUTSfW69rMm7DAOLDz05KaEJJSI5+
// YbDPr2S82A2ByHHQt+Vu168sGz4noXTTSX2HIdVutaR/IJ0a5pNOa1vRR4MUW/ZO
// YaRir3yC5YXgcFLwwQaifNZ3lZ7WndbYEjTGOcieQQ81IUP2221PZCJI52S95nYm
// VfslsLiPhOFH7XhGSqelGYDi0cKyl0p6dKvYxFswfKKLTuWnu2BEFLjVq4S5Y9Ob
// SGm0KL/8g7pAqjac2sMzzhHtxZ+7k8tynzAf4slJJhHMm5U4DcSelTe5zOkprCJg
// muyv0H1Acb3tfXsBwfURjiE0cvSMhfum5I5epF+f139tsr1zNF24F2WgvEZZbXcG
// g1LveGCJ/0BY0pzE71DU2SYiUhl+HGDv2u32vJO80jCDf2lu7izEt544a+XE+2X0
// zVpwjNQGa2Nd4ApGosa1fbcS5MsEdbyrjMf80SAmOeb9g3y5Zt2MY7M0Njxbvmmd
// mF20PkklpH0L01lhg2AGma4o4ojolYHzDoM5a531xTw1fZIdgbSTowz0SlAHAKD3
// c2KCCsKlBbFcqy4q7yNX63SqmI3sNA3kTH9CQJdBloRvV103Le9C0iY8CAWQmow5
// N/sDJUabgOMqe9yopSjb7LUCAwEAAQ==
// -----END PUBLIC KEY-----
};
To sign a message:
const generateHash = async (message) => {
const encoder = new TextEncoder();
const buffer = encoder.encode(message);
const digest = await window.crypto.subtle.digest('SHA-256', buffer);
return digest;
};
const signMessage = async (message) => {
const { privateKey } = keys;
const digest = await generateHash(message);
const signature = await window.crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, digest);
return signature;
};
To verify the message in browser:
const verifyMessage = async (signature, message) => {
const { publicKey } = keys;
const digest = await generateHash(message);
const result = await window.crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, digest);
return result;
};
When the keys are created, the public key is exported and sent to the server. Later:
const message = 'test';
const signature = await signMessage(message);
await verifyMessage(signature, message); // true
sendToServer(message, bufferToHex(signature));
Since the signature is an ArrayBuffer, I convert it to hex with the following code:
const bufferToHex = input => [...new Uint8Array(input)]
.map(v => v.toString(16).padStart(2, '0')).join('');
On the server (NodeJS 8.11.0):
const publicKey = getPublicKey(userId);
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(message, 'utf-8');
const sigBuf = Buffer.from(signature, 'hex');
verifier.verify(publicKey, sigBuf); // false
I've been chasing down this issue for days and just cannot seem to figure it out. I've tried both RSA-SHA256 and sha256WithRSAEncryption for verification to no avail. Furthermore, no errors are being thrown. Any help would be enormously appreciated!
So I don't fully understand why this is the case, but to solve the issue I needed to convert the SHA hash from an ArrayBuffer into a hex string, then read back into an array buffer using TextEncoder.
const generateHash = async (message) => {
const encoder = new TextEncoder();
const buffer = encoder.encode(message);
const digest = await window.crypto.subtle.digest('SHA-256', buffer);
// Convert to hex string
return [...new Uint8Array(digest)]
.map(v => v.toString(16).padStart(2, '0')).join('');;
};
Then when signing:
const signMessage = async (message) => {
const encoder = new TextEncoder();
const { privateKey } = keys;
const digest = await generateHash(message);
const signature = await window.crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, encoder.encode(digest));
return signature;
};
The signature no longer verifies on the client but it verifies in node. 🤷‍♂️
The issue is that you are signing the hash of hash of your input when you should actually be signing hash of your input. SubtleCrypto internally hashes the input. There is no need for you to provide hashed input. Since you provided hashed input as an argument, SubtleCrypto hashed it again and then signed it which led to mismatch of signatures.
It may be useful to note that both crypto and crypto.subtle hash the message before signing, so hashing separately is not necessary.
Assuming you were able to create and load your keys properly, your code should look like this.
Browser
const message = 'hello'
const encoder = new TextEncoder()
const algorithmParameters = { name: 'RSASSA-PKCS1-v1_5' }
const signatureBytes = await window.crypto.subtle.sign(
algorithmParameters,
privateKey,
encoder.encode(message)
)
const base64Signature = window.btoa(
String.fromCharCode.apply(null, new Uint8Array(signatureBytes))
)
console.log(base64Signature)
// TiJZTTihhUYAIlOm2PpnvJa/+15WOX2U0iKJ2LXsLecvohhRIWnwFfdHy4ci10mcv/UQgf2+bFf9lfFZUlPPdzckBNfXIqAjafM8XquJiw/t1v+pEGtJpaGASlzuWuL37gp3k8ux3l6zBKKbBVPPASkHVhz37uY1AXeMblfRbFE=
Node
This implementation is using crypto, but you could use the crypto.subtle to be more similar to the browser javascript syntax
const crypto = require('crypto')
const message = 'hello'
const base64Signature = 'TiJZTTihhUYAIlOm2PpnvJa/+15WOX2U0iKJ2LXsLecvohhRIWnwFfdHy4ci10mcv/UQgf2+bFf9lfFZUlPPdzckBNfXIqAjafM8XquJiw/t1v+pEGtJpaGASlzuWuL37gp3k8ux3l6zBKKbBVPPASkHVhz37uY1AXeMblfRbFE='
const hashingAlgorithm = 'rsa-sha256'
const doesVerify = crypto.verify(
hashingAlgorithm,
Buffer.from(message),
{ key: publicKey },
Buffer.from(base64Signature, 'base64')
);
console.log(doesVerify)
// true
Not a direct answer but it might just be easier to use this: https://www.npmjs.com/package/#peculiar/webcrypto so your code on client and server is consistent while addressing this problem at the same time.

Global scope variable initialization in google cloud function

I want to store a secret key using Google Cloud KMS and use it in Google Cloud Function. First I will encrypt my key and store it in environment variable
If I decrypt my secret key like link, it returns Promise.
Is my variable guaranteed to be finished initializing when my function is deployed and called?
I'm the author of that code snippet and the corresponding blog post. For the post history, here's the full snippet the OP is referring to:
const cryptoKeyID = process.env.KMS_CRYPTO_KEY_ID;
const kms = require('#google-cloud/kms');
const client = new kms.v1.KeyManagementServiceClient();
let username;
client.decrypt({
name: cryptoKeyID,
ciphertext: process.env.DB_USER,
}).then(res => {
username = res[0].plaintext.toString().trim();
}).catch(err => {
console.error(err);
});
let password;
client.decrypt({
name: cryptoKeyID,
ciphertext: process.env.DB_PASS,
}).then(res => {
password = res[0].plaintext.toString().trim();
}).catch(err => {
console.error(err);
});
exports.F = (req, res) => {
res.send(`${username}:${password}`)
}
Because Node is an asynchronous language, there is no guarantee that the variables username and password are fully initialized before function invocation as-written. In that snippet, I optimized for "decrypt at function boot so each function invocation runs in constant time". In your example, you want to optimize for "the function is fully initialized before invocation" which requires some re-organization of the code.
One possible solution is to move the lookup into a Node function that is invoked when the GCF function is called. For example:
const cryptoKeyID = process.env.KMS_CRYPTO_KEY_ID;
const kms = require('#google-cloud/kms');
const client = new kms.v1.KeyManagementServiceClient();
let cache = {};
const decrypt = async (ciphertext) => {
if (!cache[ciphertext]) {
const [result] = await client.decrypt({
name: cryptoKeyID,
ciphertext: ciphertext,
});
cache[ciphertext] = result.plaintext;
}
return cache[ciphertext];
}
exports.F = async (req, res) => {
const username = await decrypt(process.env.DB_USER);
const password = await decrypt(process.env.DB_PASS);
res.send(`${username}:${password}`)
}
Note that I added a caching layer here, since you probably don't want to decrypt the encrypted blob on each invocation of the function.

AWS Lambda TypeError: callback is not a function

I'm trying to make twilio access token in AWS lambda, but I get the error "callback is not a function". How can I fix it?
const AccessToken = require('twilio').jwt.AccessToken;
const VoiceGrant = AccessToken.VoiceGrant;
exports.generateToken = function(identity, callback) {
// Used when generating any kind of tokens
const accountSid = 'xxxxxxxxx';
const apiKey = 'xxxxx';
const apiSecret = 'xxx';
// Used specifically for creating Voice tokens
const pushCredSid = 'xxx';
const outgoingApplicationSid = 'xxxxx';
// Create an access token which we will sign and return to the client,
// containing the grant we just created
const voiceGrant = new VoiceGrant({
outgoingApplicationSid: outgoingApplicationSid,
pushCredentialSid: pushCredSid
});
// Create an access token which we will sign and return to the client,
// containing the grant we just created
const token = new AccessToken(accountSid, apiKey, apiSecret);
token.addGrant(voiceGrant);
token.identity = identity;
console.log('Token:' + token.toJwt());
callback(null, token.toJwt());
};
As Roland Starke said, it's worth changing this exports.generateToken = function(identity, callback) to exports.generateToken = function(event, context, callback) and everything will work fine.

Resources