How to verify a JWT signature using Node-jose - javascript

I am trying to use node-jose to verify signatures of my JWTs. I know the secret, but am having trouble converting this secret into a JWK used for the verification.
Here is an example of how I am trying to create my key with my secret and verify my token. This results in Error: no key found.
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJpYWxfbnVtYmVyIjoiNWYxMGExNjMtMjk2OC00ZDZkLWIyZDgtOGQxNjQwMDNlMmQ0Iiwic2VxIjo1MTI4MTYsIm5hbWUiOiJOYW1lMSIsImlkIjo2NTQsImRlc2NyaXB0aW9uIjoiVGVzdCBEZWNvZGluZyJ9.ahLaTEhdgonxb8rfLG6NjcIg6rqbGzcHkwwFtvb9KTE"
let secret = "SuperSecretKey"
let props = {
kid: "test-key",
alg: "HS256",
use: "sig",
k: secret,
kty: "oct"
}
let key;
jose.JWK.asKey(props).then(function(result) {key = result})
jose.JWS.createVerify(key).verify(token).then(function(result){console.log(result)})
Do I need to modify my token to include the kid header somewhere? Am I generating the key correctly from the known secret for this library?

You have three problems with your code.
due to the asynchronous nature of the promises, key gets a value when the promise is fulfilled (in the .then part), but that happens after the next line gets called.
Place a console.log(key) directly after the line jose.JWK.asKey(... and you see you get "undefined" as a result. So there is actually no key.
the k value in a JWK is treated as a Base64Url encoded octet. When you sign the token, you have to use the base64url decoded value of k, but not k directly.
the secret "SuperSecretKey" is too short for node.jose. For the HS256 algorithm, the secret has to be 256 bits long. node.jose seems to be quite strict, compared to other libs.
To solve the first problem, you can either nest the calls (which quickly becomes hard to read, or use the async/await syntax like shown below:
var jose = require('node-jose')
async function tokenVerifyer()
{
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJpYWxfbnVtYmVyIjoiNWYxMGExNjMtMjk2OC00ZDZkLWIyZDgtOGQxNjQwMDNlMmQ0Iiwic2VxIjo1MTI4MTYsIm5hbWUiOiJOYW1lMSIsImlkIjo2NTQsImRlc2NyaXB0aW9uIjoiVGVzdCBEZWNvZGluZyJ9.KK9F14mwi8amhsPT7ppqp_yCYwwOGcHculKByNPlDB8"
let secret = "SuperSecretKeyThatIsLongEnough!!" // A 32 character long secret to get 256 bits.
let props = {
kid: "test-key",
alg: "HS256",
use: "sig",
k: "cynZGe3BenRNOV2AY__-hwxraC9CkBoBMUdaDHgj5bQ",
//k : jose.util.base64url.encode(secret), // alternatively use above secret
kty: "oct"
}
let key = await jose.JWK.asKey(props)
let result = await jose.JWS.createVerify(key).verify(token)
}
tokenVerifyer()
In the above example, k is a key generated on https://mkjwk.org/ and the token was created with that key on https://jwt.io (check 'secret base64 encoded'). Alternatively, you can use your own secret, but have to make sure it's long enough.
Do I need to modify my token to include the kid header somewhere?
The small example above works without putting the kid in the token. For any real applications, you would usually add the kid into the token header. Your keystore could have more keys or rotating keys and the kidhelps to select the correct one.

Related

How to use bcrypt to encrypt a date and decrypt it for comparison?

I am sending a token to user email like below and using bcrypt for this as an encrypt/decrypt mechanism.
const token = await bcrypt.hash(joinDate, 10);
When the user clicks on the link in email, I get the above token back as that token is a part of
/api/unsubscribe?userId="abcd"&token="token_that_was_generated_using_bcrypt_and_sent_to_user"
const {userId, token} = req.query;
In the api, I am comparing joinDate obtained from database vs token sent by req.query but it never matches.
const joinDate = user.joinDate.toString();
const tokenValidated = await bcrypt.compare(joinDate, token)//this is always false although us generated from same joinDate field
Why is tokenValidated always false although it was generated using the same field joinDate?
Your use of bcrypt is not secure. Anyone can brute-force a few hundred or a few thousand dates and cause any user to become unsubscribed.
I assume your motivation is to avoid having to create a new table in your database and store a random token for every email you've sent out.
If so, the proper tool to use is HMAC.
Your URL should be of the form: /api/unsubscribe?userId=abcd&mac=....
Come up with a secret key known only to your server. This secret key will be used to create and authenticate all unsubscribe requests. Only use this key for authenticating unsubscribe requests.
Perform HMAC-SHA512 on the user id, with the HMAC output truncated to 128 bits. Then base64-encode the 128 bits and set it as the mac parameter in the URL.
HMAC means to create a hash-based message authentication code, which will confirm to your server that it must have created and emailed out that 'mac'.
Now, your server can authenticate each response, because only someone with knowledge of the server's secret key can produce a valid unsubscribe link.
Your logic of using bcrypt for hash and verifying on the other end should work. The only possible thing that can prevent it from working is either:
1 - the joinDate here: const token = await bcrypt.hash(joinDate, 10);
is not ===
to joinDate here:const tokenValidated = await bcrypt.compare(joinDate, token)
( maybe differes in ", ' or different format orsomething )
2 - or the token is kinda different in ' or " when passing through / read from queryparam; (e.g. you token is going "'token_that_was_generated_using_bcrypt_and_sent_to_user'" for comparison.)

Generate jwt web auth tokens in ember-cli-mirage (or any JavaScript) for use in your ember app

I work on an Ember team that implemented djangorestframework-simplejwt for our API security. It's a good API solution, but our mirage user was getting logged out after a period of time and could not log back into our app (for testing, development). I traced the problem down to how jwt works, and the fact that I had pasted static jwt tokens in our mirage config /login endpoint.
jwt or JSON Web Tokens contain an expiration date, set on the server. Once that expiration date passes, the client cannot be auth'ed into the app anymore, until the server sends a new token with a future expiration date. This was a problem for our mirage ENV, because the mirage endpoint for /login was returning a static jwt token which I had copy/pasted from our backend response. The workaround was to get new tokens from our backend, paste them into our mirage config and use them until they expire, which is not a true permanent solution to this problem.
After a LOT of trial and error (and learning way too much about jwt), I came up with this solution, which creates a valid jwt token with an expiration date 7 days in the future. It only requires crypto-js (npm install crypto-js), a very lightweight library with many crypto functions, but no dependencies:
import CryptoJS from 'CryptoJS';
const generateTokens = function(secretKey) { // fn to generate jwt access and refresh tokens with live date (+7 days) expiration
let newEpochDate = new Date().valueOf();
newEpochDate += 6.048e8; // add 7 days padding
newEpochDate = Math.trunc(newEpochDate / 1000); // convert to Java epoch date value
let tokenObjBase = {
'typ': 'JWT',
'alg': 'HS256'
};
let tokenObjAccess = {
'token_type': 'access',
'exp': newEpochDate,
'jti': '83bc20a2fb564aa8937d167586166f67',
'user_id': 24865
};
let tokenObjRefresh = {
'token_type': 'refresh',
'exp': newEpochDate,
'jti': '83bc20a2fb564aa8937d167586166f67',
'user_id': 24865
};
let base64urlEncode = function (obj) {
let base64url = CryptoJS.enc.Utf8.parse(JSON.stringify(obj)).toString(CryptoJS.enc.Base64);
base64url = base64url.replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
return base64url;
}
let tokenBase = base64urlEncode(tokenObjBase);
let tokenAccess = base64urlEncode(tokenObjAccess);
let tokenRefresh = base64urlEncode(tokenObjRefresh);
let signatureAccessArray = CryptoJS.HmacSHA256(tokenBase + '.' + tokenAccess, secretKey); // crypto-js returns a "wordarray" which must be stringified back to human readable text with a specific encoding
let signatureAccess = signatureAccessArray.toString(CryptoJS.enc.Base64).replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
let signatureRefreshArray = CryptoJS.HmacSHA256(tokenBase + '.' + tokenRefresh, secretKey);
let signatureRefresh = signatureRefreshArray.toString(CryptoJS.enc.Base64).replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
return {tokenRefresh: tokenBase + '.' + tokenRefresh + '.' + signatureRefresh, tokenAccess: tokenBase + '.' + tokenAccess + '.' + signatureAccess};
}
export default function() { // ...rest of mirage/config.js
// you may also need this in your ember-cli-build:
app.import('node_modules/crypto-js/crypto-js.js', {
using: [
{ transformation: 'amd', as: 'CryptoJS' }
]
});
This fn can be called by any route in the config file: let tokens = generateTokens('thisisnotarealsecretkey');
It returns an object with an "access" token and a "refresh" token, the two token types required by our django jwt setup. Customize the tokenObjBase, tokenObjAccess and tokenObjRefresh to meet your backend's setup.
The basic structure of a jwt token can be found here: https://jwt.io/
To summarize, a jwt token has three strings, separated by two periods (.).
The first string is the tokenObjBase passed through JSON.stringify(), then converted to a base64URL value. That URL part is important, because regular base64 encodings don't remove the =, + and / chars, which are not "web safe." The tokenObjBase must contain typ and alg properties and nothing else.
The second string is your "payload" (here, tokenObjAccess or tokenObjRefresh) and usually contains user info (name, id, etc), and also an epoch date value which represents the expiration date of the token. That payload obj, like the first, is passed through JSON.stringify(), then converted to a base64URL value. DO NOT put sensitive data in these first two objs, they are not "encrypted" at all. Base64 encoding can be reversed by anyone with a computer and Google.
The third string is the jwt "signature." It is created by concatenating the first two base64 strings with a period (.) in the middle, then passing them through the HS256 encryption algorithm (HMAC-SHA256).
Then all three strings (two base64URL strings and the HS256 encrypted string) are concatenated: base64URL(tokenObjBase) + '.' + base64URL(tokenObjPayload) + '.' + signatureHS256
Hope this helps anyone having issues with jwt permanently logging their mirage users out of their Ember applications!

Decryption Issue with Node Package "node-rsa"

I am attempting to implement simple public key cryptography with this library's RSA functions, but decryption seems to be broken.
I have two "users", Alice and Bob. Both Alice and Bob (code in separate files) create a new empty key via const key = new nodeRSA(). Then, they both generate a 2048 bit public and private key pair via the function key.generateKeyPair(2048). They both then give each other their public keys by exporting them from the key with key.exportKey('pkcs8-public-pem') and storing them into separate files and reading them in with fs. Alice then attempts to write a message to bob by passing both the string message and bob's public key into the function below
module.exports.writeMessage = (message, key) => {
const k = new rsa(key, 'pkcs8-public-pem')
const cipherText = k.encrypt(message, 'hex');
console.log('Saving "${cipherText}" to ctext.txt');
fs.writeFileSync('ctext.txt', cipherText);
};
Then, when bob goes to read the message, he passes in his full key and decodes the message from ctext.txt as shown in the function below
module.exports.readMessage = key => {
const encryptedMessage = fs.readFileSync('ctext.txt');
const message = key.decrypt(encryptedMessage, 'utf8');
return message;
};
Encryption works just fine, and Alice is able to send the ciphertext to ctext. The problem comes when bob calls the readMessage function and attempts to decipher the text. Both the Alice and Bob programs were activated and their keys remained unchanged throughout this process. The below error occurs on deciphering:
Error: Error during decryption (probably incorrect key). Original error: Error: Incorrect data or key
at NodeRSA.module.exports.NodeRSA.$$decryptKey (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/node_modules/node-rsa/src/NodeRSA.js:301:19)
at NodeRSA.module.exports.NodeRSA.decrypt (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/node_modules/node-rsa/src/NodeRSA.js:249:21)
at Object.module.exports.readMessage.key [as readMessage] (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/Part2/rsaReadWrite.js:7:25)
at inquirer.prompt.then (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/Part2/bob.js:42:43)
at processTicksAndRejections (internal/process/next_tick.js:81:5)
I really cannot see how the system thinks it is the incorrect key, since Alice encrypted the message using Bob's public key, and Bob is decoding the message using is private key. I've tried changing padding schemes to no avail as well. Any help would be appreciated greatly. For reference, the library's github is here: https://github.com/rzcoder/node-rsa
As mentioned by Maarten, the issue was that writeFileSync was encoding my cipher text in utf8 rather than the format the cipher text was in. This resulted in reading back incorrect cipher text, causing the key or data mismatch exception. Changing the default encoding for the function to hex solved the issue.

Reply to an email in Gmail with AppScript with changed recipients ends up in a new thread

I have an email in my mailbox and I want the AppScript program to reply to it with just me and a special google group as the recipients. The purpose of this is communication of the program with me as the program replies to the message once it has processed it with necessary details about the processing in the reply body. There might also be other recipients apart from me in the original message and I don't want the program to send the reply to them.
So I need to reply with a changed set of recipients. When I do it in the Gmail GUI it works just fine, I hit reply, change the recipients, send the message and the reply ends up in the original thread. However when I do it in the script the reply always ends up in a new thread. Originally I thought Gmail decides based on the subject of the email but it seems there's more to it (perhaps it has recently changed as I think it used to work that way).
I tried multitude of slightly different approached, one of them being:
var messageBody = "foo";
var newRecipients = "me#gmail.com, my-group#gmail.com";
var messageToReplyTo = ...;
var advancedParams = {from : "my-alias#gmail.com"};
var replyDraft = messageToReplyTo.createDraftReply(messageBody);
var replySubject = replyDraft.getMessage().getSubject();
var replyBody = replyDraft.getMessage().getBody();
replyDraft.update(newRecipients, replySubject, replyBody, advancedParams);
replyDraft.send();
There are a couple fun things you need to do in order to achieve this, but you can do it without too much trouble. You should definitely review the guide to Drafts.
Per the API spec:
In order to be part of a thread, a message or draft must meet the following criteria:
The requested threadId must be specified on the Message or Draft.Message you supply with your request.
The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard.
The Subject headers must match.
To start, you need to get a reference to the draft you want to update. This is probably simplest by using GmailApp:
const thread = /** get the thread somehow */;
const newBody = /** your plaintext here */;
const reply = thread.createDraftReply(newBody);
The primary issue with Gmail & Drafts is that a Draft is an immutable message to server resources. If you change any of it, you change all of it. Thus, to change a header value such as the recipient address, you need to completely rebuild the message. This is why using the GmailApp methods to update a draft fail to maintain the existing thread information - you can't specify it as one of the advanced options for building the new message. Thus, you must use the Gmail REST API for this task:
const rawMsg = Gmail.Users.Drafts.get("me", reply.getId(), {format: "raw"}).message;
To update a draft, you need to supply an RFC 2822 formatted message encoded in base64. If you are comfortable converting the rich format message parts into such a valid string, by all means work with the non-raw format, as you have direct access to the headers in the message.payload.
To work with the raw message, know that Apps Script casts the described base64 encoded string to a byte array in the above call. The leap is then to treat that byte array as string bytes, specifically, charCodes:
const msg_string = rawMsg.raw.reduce(function (acc, b) { return acc + String.fromCharCode(b); }, "");
console.log({message: "Converted byte[] to str", bytes: rawMsg.raw, str: msg_string});
Once you have the message as a string, you can use regular expressions to update your desired headers:
const pattern = /^To: .+$/m;
var new_msg_string = msg_string.replace(pattern, "To: <....>");
// new_msg_string += ....
Since the Gmail API endpoint to update a Draft expects a base64 web-safe encoded string, you can compute that:
const encoded_msg = Utilities.base64EncodeWebSafe(new_msg_string);
And the only remaining bit is to perform the call (and/or send the updated draft).
const resource = {
id: <draft id>, // e.g. reply.getId()
message: {
threadId: <thread id>, // e.g. thread.getId()
raw: encoded_msg
}
}
const resp = Gmail.Users.Drafts.update(resource, "me", reply.getId());
const sent_msg = Gmail.Users.Drafts.send({id: resp.id}, "me");
console.log({message: "Sent the draft", msg: sent_msg});
I don't claim that the handling of the Byte array returned from the Message.raw property is 100% correct, only that it seems correct and didn't result in any errors in the test message I sent. There may also be an easier approach, as the Apps Script service has a Drafts.update endpoint which accepts a Blob input and I have not investigated how one would use that.

Interop between node-jose (js) and jwcrypto (python) using EC keys?

I'm struggling to produce a JWE in jwcrypto equivalent to that in node-jose with the same key. The goal is to produce a key in node-jose and export the pubkey to jwcrypto to encrypt a payload, which will then be consumed by node-jose and decrypted.
My test entirely in node-jose works fine:
var jose = require("node-jose")
var keyStore = jose.JWK.createKeyStore()
keyStore.generate('EC', 'P-521').then(function (result) {
// Use exported key to encrypt something (so we see the same thing jwcrypto does)
jose.JWK.asKey(result.toJSON()).then(function(result) {
jose.JWE.createEncrypt(result).update('this is a test payload').final().then(function (result) {
jose.JWE.createDecrypt(keyStore).decrypt(result).then(function (result) {
// Result is good
console.log(result)
})
})
})
However, when I do the same in python, node-jose produces a different JWE:
key = jwk.JWK(**json.loads(the_exported_key))
# This key looks exactly the same as the exported key in node-jose
print(key.export(private_key=False))
payload = "this is a test payload"
header = {
'alg': 'ECDH-ES',
'enc': 'A128CBC-HS256',
}
my_jwe = jwe.JWE(payload.encode('utf-8'), header)
my_jwe.add_recipient(key)
When node-jose tries to decrypt my_jwe, it fails with "Error: no key found". Strangely (or not, this is my first time using JWEs...), the two encryption results are (see examples below). I think I'm missing how to get jwcrypto to, like node-jose, not require 'header' values, but when I pull those it complains.
node-jose example (junk data):
{
ciphertext: "1e7YX6hNDJWJELhHTNXEOg",
iv: "oQZZq2smHX8u8MMwoC6NBA",
protected: "eyJhbGciOi".....(very long string),
tag: "3NfEqx9f2ivL8QodG5Duaw",
}
jwcrypto (junk data):
{
ciphertext: "7ldKnkcsLZUy-SXFRv_HpkWOsb-YUUlNFv-4M5yZhCA",
iv: "1uErMiK_RWcaPXPCPq12Uw",
header: {
alg: "ECDH-ES",
enc: "A128CBC-HS256",
epk: {
crv: "P-521",
kty: "EC",
x: different from the exported key, I assume this is expected 'epk',
y: different from the exported key, I assume this is expected 'epk',
},
kid: "JCU3sWKfirVybFbpy2NPOnq-4-43JiemRZLO5dmPMVo"
},
tag: "51AMFyCJld5uPyMFLLl-sw",
}
The results you got with jwcrypto or node-jose look compliant with the RFC7516.
The only difference is that node-jose set your header in the protected member (integrity protected header) whereas jwcrypto set it in the header member (per-recipient unprotected header).
My understanding is that node-jose throws an error because it cannot find the public key in the header (epk member). It only checks the protected member and not other headers (header and also unprotected members if present) which is not compliant with the RFC7516 section 2 paragraph 4.:
let the JOSE Header be the union of the members of the JWE Protected Header, the JWE Shared Unprotected Header and the corresponding JWE Per-Recipient Unprotected Header
From my point of view, when a JWE is created for only one recipient, there is no reason to set the epk member (as well as the alg and enc members) in an unprotected header. The presence of those unprotected headers will prevent you from using the JWE Compact Serialization. So the behaviour of jwcrypto should be changed.
I don't know how these two libraries work, however there are two ways to fix that issue:
Force jwcrypto to use the integrity protected header instead of the unprotected one (best).
Ask node.jose to take into account the other headers (good but may take some time)

Categories