Signing JWT - do I do it wrong? - javascript

I'm trying to make a JWT generator in JavaScript for educational purposes. There is a jwt.io tool to create and/or validate JWT.
I'm struggling to get my results match the results from the validator. The problem is the signature.
Here's my code:
function base64url(input) {
return btoa(typeof input === 'string' ? input : JSON.stringify(input))
.replace(/=+$/, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
const JWT = {
encode(header, payload, secret) {
const unsigned = [base64url(header), base64url(payload)].join('.');
return [unsigned, base64url(sha256.hmac(secret, unsigned))].join('.');
}
};
To encrypt HMAC SHA256 I'm using js-sha256 library with sha256.hmac(key, value) prototype. I compared it with online tools and it works fine.
Now, I test it with the following code:
const jwt = JWT.encode(
{
alg: 'HS256',
typ: 'JWT'
},
123,
'xxx'
);
The result I get is:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.MTIz.NzhlNTFmYzUxOGQ2YjNlZDFiOTM0ZGRhOTUwNDFmMzEwMzdlNmZkZWRhNGFlMjdlNDU3ZTZhNWRhYjQ1YzFiMQ
On the other hand, the result from jwt.io is:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.MTIz.eOUfxRjWs-0bk03alQQfMQN-b97aSuJ-RX5qXatFwbE
As you can see, the two out of three chunks of JWT are identical in my result and jwt.io result. The signature is different and if you ask me, the signature generated by it is surprisingly short. That tool also marks my own JWT as invalid.
I checked with online HMAC SHA256 generators and it looks like my code creates a valid signature, so:
base64url(sha256.hmac('xxx', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.MTIz')) ===
'NzhlNTFmYzUxOGQ2YjNlZDFiOTM0ZGRhOTUwNDFmMzEwMzdlNmZkZWRhNGFlMjdlNDU3ZTZhNWRhYjQ1YzFiMQ'
Is jwt.io just broken or does it do it some other way?

I wouldn't say you're doing it wrong, but you missed a small but important detail.
The result from jwt.io is correct and the hash you calculate is also correct. But the signature you create with your hash is not correct.
The hash you calculate with sha256.hmac(secret, unsigned) is a large number but the return value of the function is a hexadecimal string representation of that large number. For the signature you need to base64url encode the original number instead of it's string representation.
I modified your code, so that it encodes the hash value directly to base64url (node.js version):
const JWT = {
encode(header, payload, secret) {
const unsigned = [base64url(header), base64url(payload)].join('.');
const hash = sha256.hmac(secret, unsigned);
console.log(hash);
var signature = new Buffer.from(hash, 'hex').toString('base64').replace(/\+/g,'-').replace(/\=+$/m,'');
return [unsigned, signature].join('.');
}
};
or, if you don't use node.js, you can use this instead (as suggested by Robo Robok):
const JWT = {
encode(header, payload, secret) {
const unsigned = [base64url(header), base64url(payload)].join('.');
return [unsigned, base64url(sha256.hmac(secret, unsigned).replace(/\w{2}/g, byte => String.fromCharCode(parseInt(byte, 16))))].join('.');
}
};
The result is a token, which is identical to the one created with jwt.io:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.MTIz.eOUfxRjWs-0bk03alQQfMQN-b97aSuJ-RX5qXatFwbE
See also my answer here, in which I explained the steps to compare the results from different tools.

Related

How to get JSON value from PHP?

I'm trying to get JSON value from PHP but if one of the property values contains a forward slash "/" it fails (I'm using GET to send data for this particular scenario).
Here's how I'm sending the data (This works just fine when I don't send "/").
UI side
const dataObj = {
description: 'my / description',
buyer: 'Mike Brown'
};
const dataString = JSON.stringify(dataObj);
fetch(`http://localhost:8000/clients/${dataString}`)
.then((response) => response.text())
.then((responseData) => {
......
});
PHP side:
Route::get('/clients/{data}', function($data) {
// This line below fails ONLY if value of description property contains "/"
// otherwise it works just fine
$dataToBeSaved = json_decode($data, true);
});
Yes, I did some research on json_decode, but nothing very clear. Can anyone point me on the right direction? Thanks a lot in advance!
It's not really a good idea to pass JSON data in a URL, and you can't do it directly because it will contain characters that have meaning in URLs (e.g. /, ?, =, etc.). But if you must you have a couple options:
You can URL encode the string and pass it as a parameter. This wouldn't work with the route you have, but it has the benefit of not needing to do anything else. You can just get the value from the parameter.
const dataObj = {
description: 'my / description',
buyer: 'Mike Brown'
};
const dataString = encodeURIComponent(JSON.stringify(dataObj));
console.log(`https://example.com/clients?data=${dataString}`);
Or you can base64 encode it. This doesn't by default create URL safe strings, so you'll have to replace a few characters to make it URL safe. That also means you'll have to do the reverse on the server. This solution will work with your route.
const base64UrlEncode = function(str) {
return btoa(str)
// this character have meaning in URLs so we need to replace them
// with something else. we'll have to reverse this on the server.
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
};
const dataObj = {
description: 'my / description',
buyer: 'Mike Brown'
};
const dataString = base64UrlEncode(JSON.stringify(dataObj));
console.log(`https://example.com/clients/${dataString}`);
And to decode on the server:
function base64UrlDecode($encodedStr) {
// undo the character replacements we did when encoding the string
$unreplace1 = str_replace('-', '+', $encodedStr);
$unreplace2 = str_replace('_', '/', $unreplace1);
return base64_decode($unreplace2);
}
Route::get('/clients/{data}', function($data) {
$dataToBeSaved = json_decode(base64UrlDecode($data), true);
});
One thing to note with this solution is that web servers usually have a limit for the length of URLs (e.g. Apache's default is 8,177 characters). Also, there is usually a limit to the size of the "filename" in the URL (the last component in the path). Apache's default is 255 bytes/characters. So if your base64 encoded JSON is longer than 255 characters, it won't work. A better solution is to pass the data as part of the request body (i.e. as a POST request). That way you won't have a limit and you won't need to encode it beyond converting it to JSON.

Swift 5 - How to convert LONGBLOB/Buffer into Data

I am currently working on a project for school.
I have written an API using Express connected to a mysql database. And now I am writing the iOS app.
My problem is that I need to save profile pictures. So I saved the png data of the picture into a **LONGBLOB** into db and I want to recreate the image into a **UIImage**.
To do that I am trying to convert the buffer into ```Data```
So, the API is returning a buffer created that way:
let buffer = Buffer.from(ppData.data, 'binary').toString('base64');
And on the iOS side I tried:
guard let data = dict["data"] as? Data else {return nil}
Where dict["data"] is the buffer returned by the API.
But it always enter into the "else" part.
What am i doing wrong
Edit:
As what it was said in comments, I decoded the Base64 encoded string. Now the data are decoded but creating a UIImage from it, fails, without any details. What I tried is:
let image = UIImage(from: base64DecodedData)
For example:
guard let strData = dict["data"] as? String else {
return nil
}
guard let data = Data(base64Encoded: strData, options: .ignoreUnknownCharacters) else {
return nil
}
guard let picture = UIImage(data: data) else {
return nil
}
Thanks.
The mistake was not in the swift code part but in my API and database structure. After reading some MySQL and Node.js documentaion, I switched from LONGBLOB (which is totally oversized) to MEDIUMTEXT.
Also, in the API I was trying to create a buffer from binary data but not from a base64 string encoded data, so I removed this line:
let buffer = Buffer.from(ppData.data, 'binary').toString('base64');

getting crypto.subtle.encrypt to work like CryptoJS.AES.encrypt

I wrote a system that is implemented using CryptoJS.
After writing my code, I discovered crypto.subtle.encrypt which is an AES implementation built into browsers.
I want to change my code away from using CryptoJs and onto using crypto.subtle.encrypt.
Data encoded the old way (CryptoJS) has to be compatible with the new way (crypto.subtle.encrypt).
How can I achieve this?
When I wrote my original code, it looked much like this:
function cryptojs_encrypt(message) {
var key = "my password";
return CryptoJS.AES.encrypt(message, key).toString());
}
Where the "key" passed in is just a string. From what I've been able to read from other stackoverflow questions, CryptoJS converts this string into a "key" and "iv". How exactly is this achieved? I tried looking through the CryptoJS source code but couldn't find what I was looking for.
The way subtle.crypt.encrypt works, is that you have to pass in the key and iv explicitly. Here is my code:
function subtle_encrypt(message) {
var msg = new TextEncoder().encode(message);
var pass = new TextEncoder().encode('my password');
var alg = { name: 'AES-CBC', iv: pass };
crypto.subtle.importKey('raw', pass, alg, false, ['encrypt']).then(function(key){
crypto.subtle.encrypt(alg, key, msg).then(function(ctBuffer){
var string = btoa(ctBuffer);
console.log("result", string);
});
});
}
This works but returns a different result. I need to modify the arguments that go into alg which matches what CryptoJS uses when you pass in a string. How do I do this?
I've created a small library to do just that.
Embed WebCrypto.js (Minified) in your document.
Use it like this:
// Initialize the library
initWebCrypto();
// Encrypt your stuff
WebCrypto.encrypt({
data: btoa("my message"),
password: "my password",
callback: function(response){
if( !response.error ){
console.log(response.result); // Compatible with CryptoJS
}else{
console.error(response.error);
}
}
});
See https://github.com/etienne-martin/WebCrypto.swift/blob/master/www/index.html for more examples.
Source code: https://github.com/etienne-martin/WebCrypto.swift/blob/master/source.js
Hope this helps!

AWS S3 browser upload using HTTP POST gives invalid signature

I'm working on a website where the users should be able to upload video files to AWS. In order to avoid unnecessary traffic I would like the user to upload directly to AWS (and not through the API server). In order to not expose my secret key in the JavaScript I'm trying to generate a signature in the API. It does, however, tell me when I try to upload, that the signature does not match.
For signature generation I have been using http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html
On the backend I'm running C#.
I generate the signature using
string policy = $#"{{""expiration"":""{expiration}"",""conditions"":[{{""bucket"":""dennisjakobsentestbucket""}},[""starts-with"",""$key"",""""],{{""acl"":""private""}},[""starts-with"",""$Content-Type"",""""],{{""x-amz-algorithm"":""AWS4-HMAC-SHA256""}}]}}";
which generates the following
{"expiration":"2016-11-27T13:59:32Z","conditions":[{"bucket":"dennisjakobsentestbucket"},["starts-with","$key",""],{"acl":"private"},["starts-with","$Content-Type",""],{"x-amz-algorithm":"AWS4-HMAC-SHA256"}]}
based on http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html (I base64 encode the policy). I have tried to keep it very simple, just as a starting point.
For generating the signature, I use code found on the AWS site.
static byte[] HmacSHA256(String data, byte[] key)
{
String algorithm = "HmacSHA256";
KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
static byte[] GetSignatureKey(String key, String dateStamp, String regionName, String serviceName)
{
byte[] kSecret = Encoding.UTF8.GetBytes(("AWS4" + key).ToCharArray());
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
Which I use like this:
byte[] signingKey = GetSignatureKey(appSettings["aws:SecretKey"], dateString, appSettings["aws:Region"], "s3");
byte[] signature = HmacSHA256(encodedPolicy, signingKey);
where dateString is on the format yyyymmdd
I POST information from JavaScript using
let xmlHttpRequest = new XMLHttpRequest();
let formData = new FormData();
formData.append("key", "<path-to-upload-location>");
formData.append("acl", signature.acl); // private
formData.append("Content-Type", "$Content-Type");
formData.append("AWSAccessKeyId", signature.accessKey);
formData.append("policy", signature.policy); //base64 of policy
formData.append("x-amz-credential", signature.credentials); // <accesskey>/20161126/eu-west-1/s3/aws4_request
formData.append("x-amz-date", signature.date);
formData.append("x-amz-algorithm", "AWS4-HMAC-SHA256");
formData.append("Signature", signature.signature);
formData.append("file", file);
xmlHttpRequest.open("post", "http://<bucketname>.s3-eu-west-1.amazonaws.com/");
xmlHttpRequest.send(formData);
I have been using UTF8 everywhere as prescribed by AWS. In their examples the signature is on a hex format, which I have tried as well.
No matter what I try I get an error 403
The request signature we calculated does not match the signature you provided. Check your key and signing method.
My policy on AWS has "s3:Get*", "s3:Put*"
Am I missing something or does it just work completely different than what I expect?
Edit: The answer below is one of the steps. The other is that AWS distinguish between upper and lowercase hex strings. 0xFF != 0xff in the eyes of AWS. They want the signature in all lowercase.
You are generating the signature using Signature Version 4, but you are constructing the form as though you were using Signature Version 2... well, sort of.
formData.append("AWSAccessKeyId", signature.accessKey);
That's V2. It shouldn't be here at all.
formData.append("x-amz-credential", signature.credentials); // <accesskey>/20161126/eu-west-1/s3/aws4_request
This is V4. Note the redundant submission of the AWS Access Key ID here and above. This one is probably correct, although the examples have capitalization like X-Amz-Credential.
formData.append("x-amz-algorithm", "AWS4-HMAC-SHA256");
That is also correct, except it may need to be X-Amz-Algorithm. (The example seems to imply that capitalization is ignored).
formData.append("Signature", signature.signature);
This one is incorrect. This should be X-Amz-Signature. V4 signatures are hex, so that is what you should have here. V2 signatures are base64.
There's a full V4 example here, which even provides you with an example aws key and secret, date, region, bucket name, etc., that you can use with your code to verify that you indeed get the same response. The form won't actually work but the important question is whether your code can generate the same form, policy, and signature.
For any given request, there is only ever exactly one correct signature; however, for any given policy, there may be more than one valid JSON encoding (due to JSON's flexibility with whitespace) -- but for any given JSON encoding there is only one possible valid base64-encoding of the policy. This means that your code, using the example data, is certified as working correctly if it generates exactly the same form and signature as shown in the example -- and it means that your code is proven invalid if it generates the same form and policy with a different signature -- but there is a third possibility: the test actually proves nothing conclusive about your code if your code generates a different base64 encoding of the policy, because that will necessarily change the signature to not match, yet might still be a valid policy.
Note that Signature V2 is only suported on older S3 regions, while Signature V4 is supported by all S3 regions, so, even though you could alternately fix this by making your entire signing process use V2, that wouldn't be recommended.
Note also that The request signature we calculated does not match the signature you provided. Check your key and signing method does not tell you anything about whether the bucket policy or any users policies allow or deny the request. This error is not a permissions error. It will be thrown prior to the permissions checks, based solely on the validity of the signature, not whether the AWS Access Key id is authorized to perform the requested operation, which is something that is only tested after the signature is validated.
I suggest you to create a pair auth token with permission to POST only, and send an http request like this:
require 'rest-client'
class S3Uploader
def initialize
#options = {
aws_access_key_id: "ACCESS_KEY",
aws_secret_access_key: "ACCESS_SECRET",
bucket: "BUCKET",
acl: "private",
expiration: 3.hours.from_now.utc,
max_file_size: 524288000
}
end
def fields
{
:key => key,
:acl => #options[:acl],
:policy => policy,
:signature => signature,
"AWSAccessKeyId" => #options[:aws_access_key_id],
:success_action_status => "201"
}
end
def key
#key ||= "temp/${filename}"
end
def url
"http://#{#options[:bucket]}.s3.amazonaws.com/"
end
def policy
Base64.encode64(policy_data.to_json).delete("\n")
end
def policy_data
{
expiration: #options[:expiration],
conditions: [
["starts-with", "$key", ""],
["content-length-range", 0, #options[:max_file_size]],
{ bucket: #options[:bucket] },
{ acl: #options[:acl] },
{ success_action_status: "201" }
]
}
end
def signature
Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest.new("sha1"),
#options[:aws_secret_access_key], policy
)
).delete("\n")
end
end
uploader = S3Uploader.new
puts uploader.fields
puts uploader.url
begin
RestClient.post(uploader.url, uploader.fields.merge(file: File.new('51bb26652134e98eae931fbaa10dc3a1.jpeg'), :multipart => true))
rescue RestClient::ExceptionWithResponse => e
puts e.response
end

NodeJS md5 'bytestring' like PHP md5(str, true)

I've faced with following issue: i try to convert some string str to md5 bytestring hash. In PHP we can use md5(str, true), but in JS (nodejs express) i can't find some way to receive the same result. I've included npm module js-md5, but arrayBuffer method of this module returns another result (differes from PHP md5(str, true)).
Could somebody help me, please.
Thanks
var md5 = require('md5');
console.log(md5('text'))
Use CryptoJS module :
NPM link here
And do something like :
// Requires
var crypto = require('crypto');
// Constructor
function Crypto() {
this.hash;
}
// Hash method
Crypto.prototype.encode = function(data) {
this.hash = crypto.createHash('md5').update(data);
var result = this.hash.digest('hex');
return result;
};
// Comparison method (return true if === else false)
Crypto.prototype.equals = function(data, model) {
var bool = false;
var data = data.toUpperCase();
var model = String(model).toUpperCase();
if (data == model){
bool = true;
} else {
bool = false;
}
return bool;
};
// Exports
module.exports = Crypto;
Then instantiate this "tool" object in your code and use methods.
Easy as pie, and the same thing can be done with anothers encryption methods like AES, SHA256, etc.
About the raw_output option (binary answer, padded on 16 bits) you can easily convert the returned var in binary format with a simple function, see this SO post to know how.
Have fun.
Short answer:
const crypto = require('crypto');
const buffer = crypto.createHash('md5').update(str).digest();
Long answer: you need to use NodeJS’ default crypto module (no need for a dependency here), which contains utility function and classes. It is able to create hashes (for instance MD5 or SHA-1 hashes) for you using synchronous or asynchronous methods. A short utility function named crypto.createHash(algorithm) is useful to create a hash with minimal coding. As the docs specifies:
The algorithm is dependent on the available algorithms supported by the version of OpenSSL on the platform. Examples are 'sha256', 'sha512', etc. On recent releases of OpenSSL, openssl list-message-digest-algorithms will display the available digest algorithms.
Now, this createHash function returns a Hash object, which can be used with a stream (you can feed it a file, HTTP request, etc.) or a string, as you asked. If you want to use a string, use hash.update(string) to hash it. This method returns the hash itself, so you can chain it with .digest(encoding) to generate a string (if encoding is set) or a Buffer (if it’s not). Since you asked for bytes, I believe a Buffer is what you want (Buffers are Uint8Array instances).

Categories

Resources