Is phpseclib AES-GCM encryption compatible with javascript WebCrypto? - encryption

I'm trying to encrypt/decrypt symmetrically from php/phpseclib to js/WebCrypto(SubtleCrypto). The algorithm is AES-GCM with PBKDF2 derivation and also with plain key. I had no success. The error received from the window.crypto.subtle.decrypt() function is:
OperationError: The operation failed for an operation-specific reason
RSA-OAEP works without any problems.
Did anybody do this before - is it possible at all? I didn't find anything that confirms or denies a compatibility between these modules.
Edit: adding code example
encryption:
<?php
require_once($_SERVER['DOCUMENT_ROOT'] . '/../vendor/autoload.php');
use phpseclib3\Crypt\AES;
$TEST_AES_KEY = "TWw4QCkeZEnXoCDkI1GEHQ==";
$TEST_AES_IV = "CRKTyQoWdWB2n56f";
$message = "123&abc";
$aes = new AES('gcm');
$aes->setKey($TEST_AES_KEY);
$aes->setNonce($TEST_AES_IV);
$ciphertext = $aes->encrypt($message);
$tag = $aes->getTag();
$ciphertextBase64 = base64_encode($ciphertext . $tag);
echo $ciphertextBase64;
decryption:
<!DOCTYPE html>
<html>
<script>
function _base64ToArrayBuffer(base64) {
var binary_string = atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
async function _importKeyAes(key) {
return await window.crypto.subtle.importKey(
"raw",
key,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
}
async function decryptMessageSymetric(key, data, iv) {
keyArrayBuffer = _base64ToArrayBuffer(key);
key = await _importKeyAes(keyArrayBuffer);
iv = _base64ToArrayBuffer(iv);
data = _base64ToArrayBuffer(data);
result = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv, tagLength: 128 },
key,
data
);
return new TextDecoder().decode(result);
}
TEST_AES_KEY = "TWw4QCkeZEnXoCDkI1GEHQ==";
TEST_AES_IV = "CRKTyQoWdWB2n56f";
messageEncrypted = "LATYboD/FztIKGVkiJNWHOP72C77FiY="; // result from phpseclib encryption
result = decryptMessageSymetric(TEST_AES_KEY, messageEncrypted, TEST_AES_IV);
console.log(result);
</script>
</html>

There are only two minor encoding bugs:
In the phpseclib code the key is not Base64 encoded, in the WebCrypto code it is Base64 encoded. This needs to be changed so that both sides use the same key.
For the test below I arbitrarily decide to use the WebCrypto solution, i.e. in the phpseclib code a Base64 encoding is added:
$TEST_AES_KEY = base64_decode("TWw4QCkeZEnXoCDkI1GEHQ==");
This produces a 16 bytes key so that AES-128 is applied (note that the phpseclib solution would also be possible, since the Base64 encoded key is 24 bytes in size and corresponds to AES-192; no matter which key is applied in the end, the important thing is that on both sides the same key must be used).
Running the phpseclib code again gives the following ciphertext:
7K+HAB7Ch9V4jJ1XJPM0sANXA2ocJok=
In the WebCrypto code, this new ciphertext is now used.
In the WebCrypto code the 16 bytes IV is Base64 decoded. This creates an IV that is too short for AES. Therefore the Base64 decoding is removed and a UTF-8 encoding (analogous to the phpseclib code) is performed:
iv = new TextEncoder().encode(iv);
With these changes decryption is successful:
(async () => {
function _base64ToArrayBuffer(base64) {
var binary_string = atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
async function _importKeyAes(key) {
return await window.crypto.subtle.importKey(
"raw",
key,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
}
async function decryptMessageSymetric(key, data, iv) {
keyArrayBuffer = _base64ToArrayBuffer(key);
key = await _importKeyAes(keyArrayBuffer);
iv = new TextEncoder().encode(iv); // Remove Base64 decoding
data = _base64ToArrayBuffer(data);
result = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv, tagLength: 128 },
key,
data
);
return new TextDecoder().decode(result);
}
TEST_AES_KEY = "TWw4QCkeZEnXoCDkI1GEHQ==";
TEST_AES_IV = "CRKTyQoWdWB2n56f";
messageEncrypted = "7K+HAB7Ch9V4jJ1XJPM0sANXA2ocJok="; // Apply modified ciphertext
result = await decryptMessageSymetric(TEST_AES_KEY, messageEncrypted, TEST_AES_IV);
console.log(result);
})();
Note that a static IV is a serious security risk for GCM, s. here.

Related

How to decrypt message that's encrypted in AES-GCM-256?

I need to make a client-server connection from node.js/javascript server to a client written in Rust. The message had to be encrypted with AES-GCM-256. While in Rust (version 1.67.0) I use aes-gcm crate.
The Rust code below throws an error: aead::Error. What's wrong with the cipher.decrypt() here ?
I'm sure that the node.js implementation is correct. I think that the key variable in Rust code should be the same key from the node.js (12341234123412341234123412341234)
Implementation in Node.js / sender side
const crypto = require('crypto')
const aes256gcm = (key) => {
const encrypt = (str) => {
const iv = new crypto.randomBytes(12);
const ivString = iv.toString("base64")
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let enc1 = cipher.update(str, 'utf8');
let enc2 = cipher.final();
let result = Buffer.concat([enc1, enc2, iv, cipher.getAuthTag()]).toString("base64");
return { result, iv: ivString }
};
const decrypt = (enc) => {
enc = Buffer.from(enc, "base64");
const iv = enc.slice(enc.length - 28, enc.length - 16);
const tag = enc.slice(enc.length - 16);
enc = enc.slice(0, enc.length - 28);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
let str = decipher.update(enc, null, 'utf8');
str += decipher.final('utf8');
return str;
};
return {
encrypt,
decrypt,
};
};
const cipher = aes256gcm("12341234123412341234123412341234"); // just a test key must be 32
const ct = cipher.encrypt('Hello world!!!');
console.log("encrypted message: ", ct.result)
console.log("iv / nonce : ", ct.iv)
const pt = cipher.decrypt(ct.result);
console.log("decrypted message: ", pt); // this works flawlessly!
Encryption Result by Node.js
encrypted message: Zf5aB0bbVGGX3k9Yt6x+9daxCGZO0MmwYW8VUsOY4j3gNYXP47hvfGgd
iv / nonce : fvXWsQhmTtDJsGFv
decrypted message: Hello world!!!
The decryption part (Rust) / Receiver Side
use aes_gcm::{
aead::{Aead, KeyInit, OsRng},
Aes256Gcm, Nonce,
};
use base64::{engine::general_purpose, Engine as _};
fn main() {
// master key from sender
let master = "12341234123412341234123412341234".as_bytes();
let cipher = Aes256Gcm::new_from_slice(master).unwrap();
// nonce / iv from sender
let nonce_str_base64 = "fvXWsQhmTtDJsGFv";
let nonce_str: Vec<u8> = general_purpose::STANDARD.decode(nonce_str_base64).unwrap();
let nonce = Nonce::from_slice(&nonce_str); // 96-bits; unique per message
// encrypted text from sender
let ciphertext_base64 = "Zf5aB0bbVGGX3k9Yt6x+9daxCGZO0MmwYW8VUsOY4j3gNYXP47hvfGgd";
let ciphertext = general_purpose::STANDARD.decode(ciphertext_base64).unwrap();
// gets aead::Error here
match cipher.decrypt(nonce, ciphertext.as_slice()) {
Ok(decrypted) => {
let result = String::from_utf8(decrypted).unwrap();
println!("result: {}", result);
}
Err(err) => print!("{}", err), <--- prints error: aead::Error
};
}
AES-256-GCM consists of three parts:
payload or ciphered text,
iv or nonce, a unique random number that generated once
and the tag that is part of the authentication that ensures the encrypted message has not been altered
The aes_gcm crate uses payload + tag for decryption. So the solution is to remove the iv part from the result message. I just change the sender side (Node.js) from this:
let result = Buffer.concat([enc1, enc2, iv, cipher.getAuthTag()]).toString("base64");
to this:
let result = Buffer.concat([enc1, enc2, cipher.getAuthTag()]).toString("base64");
the nonce/iv part is supplied to cipher.decrypt through function parameter

Type of expression is ambiguous without more context CryptoSwift Swift 4

When I am using Crypto Swift in Swift 4 , it is giving below error
Type of expression is ambiguous without more context
Below is the code which I have posted for encryption
func aesEncrypt(_ key: String, iv: String) throws -> String
{
let data = self.data(using: String.Encoding.utf8)
// Next Line is giving error
let enc = try! AES(key: key, iv: iv, blockMode: .CBC, padding: .pkcs7).encrypt([UInt8](data!))
let encData = Data(enc!)
let base64String = encData.base64EncodedString()
let result = String(base64String)
return result
}
extension String {
func aesEncrypt(key: String) throws -> String {
// Encryption
let data = self.data(using: .utf8)!
let password = key
let ciphertext = RNCryptor.encrypt(data: data, withPassword: password)
let encryptedData = Data(ciphertext)
let stringEncrypt: String = encryptedData.base64EncodedString()
print("encryptedData: \(stringEncrypt)")
return stringEncrypt
}
func aesDecrypt(key: String) throws -> String {
// Decryption
let data = Data(base64Encoded: self)!
let password = key
do {
let ciphertext = try RNCryptor.decrypt(data: data, withPassword: password)
let decryptedData = Data(ciphertext)
let stringDecrypt = String(bytes: decryptedData, encoding: .utf8) ?? "Could not decrypt"
print("decryptedData: \(stringDecrypt)")
return stringDecrypt
} catch {
print(error)
return "Error"
}
}
func aesEncrypt(key: String, iv: String) throws -> String {
let data: Array<UInt8> = (self.data(using: .utf8)?.bytes)!
let key: Array<UInt8> = (key.data(using: .utf8)?.bytes)!
let iv: Array<UInt8> = (iv.data(using: .utf8)?.bytes)!
do {
let encrypted = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7).encrypt(data)
let encryptedData = Data(encrypted)
let decrypted = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7).decrypt(encrypted)
let decryptedData = Data(decrypted)
let str = String.init(data: decryptedData, encoding: .utf8)
print(str ?? String())
return encryptedData.base64EncodedString()
} catch {
print(error)
return "error"
}
}
func aesDecrypt(key: String, iv: String) throws -> String {
let data: Array<UInt8> = (Data(base64Encoded: self)?.bytes)!
let key: Array<UInt8> = (key.data(using: .utf8)?.bytes)!
let iv: Array<UInt8> = (iv.data(using: .utf8)?.bytes)!
do {
let decrypted = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7).decrypt(data)
let decryptedData = Data(decrypted)
guard let value = String.init(data: decryptedData, encoding: .utf8) else {
return "error"
}
return value
} catch {
print(error)
return "error"
}
}
}
Don't use String directly, use Data or [UInt8]. The CryptoSwift has the convenient conversions helpers, to make it easier: https://github.com/krzyzanowskim/CryptoSwift#basics

Downloading Blob from Docusign Envelopes API

Using Meteor HTTP I'm able to get a response from docusign and convert to a base64 buffer.
try {
const response = HTTP.get(`${baseUrl}/envelopes/${envelopeId}/documents/1`, {
headers: {
"Authorization": `bearer ${token}`,
"Content-Type": "application/json",
},
});
const buffer = new Buffer(response.content).toString('base64');
return buffer
} catch(e) {
console.log(e);
throw new Meteor.Error(e.reason);
}
Then on the client I'm using FileSaver.js to saveAs a blob created from an ArrayBuffer via this function
function _base64ToArrayBuffer(base64) {
const binary_string = window.atob(base64);
const len = binary_string.length;
const bytes = new Uint8Array( len );
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
// template helper
'click [data-action="download"]'(e, tmpl){
const doc = this;
return Meteor.call('downloadPDF', doc, (err, pdf)=>{
if(err) {
return notify({
message: err,
timeout: 3000,
})
}
const pdfBuffer = pdf && _base64ToArrayBuffer(pdf);
console.log(pdfBuffer);
return saveAs(new Blob([pdfBuffer], {type: 'application/pdf'}), `docusign_pdf.pdf`);
});
},
The PDF is downloading with the correct size and page length, but all the pages are blank. Should I be encoding the buffer differently? Is there something else I'm missing?
When uploading documents into DocuSign you can either send the raw document bytes as part of a multipart/form-data request or you can send the document as a base64 encoded file in the document node in your request body.
However once the envelope is complete the DocuSign platform converts the doc into a PDF (if it wasn't one already) and provides that raw file. As such, you shouldn't have to base64 decode the doc so I would try removing that part of your code.

ASP.Net MVC 5 How to encrypt JWT Tokens

I have gone throw few posts about using JWT in ASP.Net MVC, which guides how to issue and consume Signed JSON Web Tokens.
Can anyone please guide how to issue and consume encrypted JWT following the JSON Web Encryption (JWE) specifications in case we need to transmit some sensitive data in the JWT payload.
Understanding JWT
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JavaScript Object Notation (JSON) object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or MACed and/or encrypted.
What JWT?
https://jwt.io/introduction/
Json Web Token Standards
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-json-web-token-25
Anatomy of JWT
https://scotch.io/tutorials/the-anatomy-of-a-json-web-token
Creating JSON Web Token in JavaScript
https://www.jonathan-petitcolas.com/2014/11/27/creating-json-web-token-in-javascript.html
Now, We understand JWT call and how we can serve it from server side.
Here i have HTML page in which I have button and also set some custom parameters.
<script src="//cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/hmac-sha256.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/enc-base64-min.js"></script>
<script language="JavaScript" type="text/javascript" src="https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js"></script>
<script type="text/javascript">
$(function () {
$("#btnJWTApi").click(function () {
// Defining our token parts
// You can use one of these, as alg
// HS256, HS386, HS512
// Always keep type as JWT
var header = {
"alg": "HS256",
"typ": "JWT"
};
var tNow = KJUR.jws.IntDate.getNow();
var tEnd = KJUR.jws.IntDate.getNow() + 60 * 5;
// dynamically pass these data using a function
var data = {
"appId": "yourAppId",
"iat": tNow,
// iat (issued at time) should be set to time when request has been generated
"exp": tEnd,
// exp (expiration) should not be more than 5 minutes from now, this is to prevent Replay Attacks
"method": "TestMethod",
"Q": "test",
"SecretKey": "MySecretKey"
};
// Secret key is used for calculating and verifying the signature.
// The secret signing key MUST only be accessible by the issuer and the User,
// it should not be accessible outside of these two parties.
// Use the Secret you set during user registration from the Plugin
var secret = btoa('MySecret ');
function base64url(source) {
// Encode in classical base64
encodedSource = CryptoJS.enc.Base64.stringify(source);
// Remove padding equal characters
encodedSource = encodedSource.replace(/=+$/, '');
// Replace characters according to base64url specifications
encodedSource = encodedSource.replace(/\+/g, '-');
encodedSource = encodedSource.replace(/\//g, '_');
return encodedSource;
}
var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);
var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);
var signature = encodedHeader + "." + encodedData;
signature = CryptoJS.HmacSHA256(signature, secret);
signature = base64url(signature);
var targetEle = $("#data");
$.ajax(
{
type: "POST",
url: "http://localhost:12345/api/v1/MyController/SecureMethod",
data: '{"token":"' + encodedHeader + "." + encodedData + "." + signature + '"}',
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
targetEle.html("<pre>" + JSON.stringify(data, null, '\t') + "</pre>");
},
error: function () {
alert('error');
}
});
});
});
</script>
This call will generate encrypted token which include appId,secret and our payload data with method name.
(Here create one common method, which call first and then according to passing data in a token further method will be call)
This will call your method SecureMethod instead of direct TestMethod.
And decrypt token.
public string SecureMethod(dynamic tokenObject)
{
//save at a time of user registration.
string applicationID = appSecret get from database;
string secretKey = appSecret get from database;
}
var bytes = Encoding.UTF8.GetBytes(secretKey);
var secret = Convert.ToBase64String(bytes);
var jwtDecryption = JsonWebToken.DecodeToObject(token, secret, true, true);
var jsonObj = JObject.FromObject(jwtDecryption);
string appId = jsonObj["appId"].Value<string>();
if (appId.Equals(applicationID)
{
object restService = new MyController();
var method = restService.GetType().GetMethod(jsonObj["method"].ToString(), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
if (method != null)
{
var parameters = method.GetParameters().Select(p => Convert.ChangeType(jsonObj[p.Name].ToString(), p.ParameterType)).ToArray();
object response = method.Invoke(restService, parameters); //your actual method should
return new JavaScriptSerializer().Serialize(response);
}
method.Invoke(restService, parameters); will have method name and parameter so it'll called your method and pass parameters.
public IHttpActionResult TestMethod([FromBody]Response model)
{
// you will get parameters in a model
return Ok();
}
Any suggestion welcome!

Get the whole response body when the response is chunked?

I'm making a HTTP request and listen for "data":
response.on("data", function (data) { ... })
The problem is that the response is chunked so the "data" is just a piece of the body sent back.
How do I get the whole body sent back?
request.on('response', function (response) {
var body = '';
response.on('data', function (chunk) {
body += chunk;
});
response.on('end', function () {
console.log('BODY: ' + body);
});
});
request.end();
Over at https://groups.google.com/forum/?fromgroups=#!topic/nodejs/75gfvfg6xuc, Tane Piper provides a good solution very similar to scriptfromscratch's, but for the case of a JSON response:
request.on('response',function(response){
var data = [];
response.on('data', function(chunk) {
data.push(chunk);
});
response.on('end', function() {
var result = JSON.parse(data.join(''))
return result
});
});`
This addresses the issue that OP brought up in the comments section of scriptfromscratch's answer.
I never worked with the HTTP-Client library, but since it works just like the server API, try something like this:
var data = '';
response.on('data', function(chunk) {
// append chunk to your data
data += chunk;
});
response.on('end', function() {
// work with your data var
});
See node.js docs for reference.
In order to support the full spectrum of possible HTTP applications, Node.js's HTTP API is very low-level. So data is received chunk by chunk not as whole.
There are two approaches you can take to this problem:
1) Collect data across multiple "data" events and append the results
together prior to printing the output. Use the "end" event to determine
when the stream is finished and you can write the output.
var http = require('http') ;
http.get('some/url' , function (resp) {
var respContent = '' ;
resp.on('data' , function (data) {
respContent += data.toString() ;//data is a buffer instance
}) ;
resp.on('end' , function() {
console.log(respContent) ;
}) ;
}).on('error' , console.error) ;
2) Use a third-party package to abstract the difficulties involved in
collecting an entire stream of data. Two different packages provide a
useful API for solving this problem (there are likely more!): bl (Buffer
List) and concat-stream; take your pick!
var http = require('http') ;
var bl = require('bl') ;
http.get('some/url', function (response) {
response.pipe(bl(function (err, data) {
if (err) {
return console.error(err)
}
data = data.toString() ;
console.log(data) ;
}))
}).on('error' , console.error) ;
The reason it's messed up is because you need to call JSON.parse(data.toString()). Data is a buffer so you can't just parse it directly.
If you don't mind using the request library
var request = require('request');
request('http://www.google.com', function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body) // Print the google web page.
}
})
If you are dealing with non-ASCII contents(Especially for Chinese/Japanese/Korean characters, no matter what encoding they are), you'd better not treat chunk data passed over response.on('data') event as string directly.
Concatenate them as byte buffers and decode them in response.on('end') only to get the correct result.
// Snippet in TypeScript syntax:
//
// Assuming that the server-side will accept the "test_string" you post, and
// respond a string that concatenates the content of "test_string" for many
// times so that it will triggers multiple times of the on("data") events.
//
const data2Post = '{"test_string": "swamps/沼泽/沼澤/沼地/늪"}';
const postOptions = {
hostname: "localhost",
port: 5000,
path: "/testService",
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data2Post) // Do not use data2Post.length on CJK string, it will return improper value for 'Content-Length'
},
timeout: 5000
};
let body: string = '';
let body_chunks: Array<Buffer> = [];
let body_chunks_bytelength: number = 0; // Used to terminate connection of too large POST response if you need.
let postReq = http.request(postOptions, (res) => {
console.log(`statusCode: ${res.statusCode}`);
res.on('data', (chunk: Buffer) => {
body_chunks.push(chunk);
body_chunks_bytelength += chunk.byteLength;
// Debug print. Please note that as the chunk may contain incomplete characters, the decoding may not be correct here. Only used to demonstrating the difference compare to the final result in the res.on("end") event.
console.log("Partial body: " + chunk.toString("utf8"));
// Terminate the connection in case the POST response is too large. (10*1024*1024 = 10MB)
if (body_chunks_bytelength > 10*1024*1024) {
postReq.connection.destroy();
console.error("Too large POST response. Connection terminated.");
}
});
res.on('end', () => {
// Decoding the correctly concatenated response data
let mergedBodyChunkBuffer:Buffer = Buffer.concat(body_chunks);
body = mergedBodyChunkBuffer.toString("utf8");
console.log("Body using chunk: " + body);
console.log(`body_chunks_bytelength=${body_chunks_bytelength}`);
});
});
How about HTTPS chunked response? I've been trying to read a response from an API that response over HTTPS with a header Transfer-Encoding: chunked. Each chunk is a Buffer but when I concat them all together and try converting to string with UTF-8 I get weird characters.

Resources