How to decrypt NodeJS crypto on the client side with a known encryption key? - cryptography

I am trying to have AES encryption on the server side, and decryption on the client side. I have followed an example where CryptoJS is used on the client side for encryption and SubtleCrypto on the client side as well for decryption, but in my case I have the encryption and decryption separated.
Suppose I have the following encryption function within React Native:
const encrypt = (str: string) => {
const iv = crypto.randomBytes(12);
const myHexToken = "0x...."
const cipher = crypto.createCipheriv('aes-256-gcm', myHexToken.slice(0,32), iv)
let encrypted = cipher.update(str, 'utf8', 'hex')
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
message: encrypted,
tag: tag.toString('hex'),
iv: iv.toString('hex'),
};
};
This json is then posted to the client through a webview postMessage.
The client side has the following javascript injected:
var myHexToken = "0x....";
window.addEventListener("message", async function (event) {
var responseData = JSON.parse(event.data);
try {
var decryptedData = await decrypt(responseData.iv, responseData.message, responseData.tag);
} catch (e) {
alert(e);
}
// ...
How can I decrypt responseData.message within the WebView through SubtleCrypto of the Web Crypto API?
I have tried various things with the following methods, but I keep getting "OperationalError":
function fromHex(hexString) {
return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}
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;
}
function fromBase64(base64String) {
return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0));
}
async function importKey(rawKey) {
var key = await crypto.subtle.importKey(
"raw",
rawKey,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
return key;
}
async function decrypt(iv, data, tag) {
var rawKey = fromHex(myHexToken.slice(0,32));
var iv = fromHex(iv);
var ciphertext = str2ab(data + tag);
var cryptoKey = await importKey(rawKey)
var decryptedData = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
cryptoKey,
ciphertext
)
var decoder = new TextDecoder();
var plaintext = decoder.decode(decryptedData);
return plaintext;
}
UPDATE 1: Added the getAuthTag implementation server side. Changed IV to have length of 12 bytes. Attempt to concatenate ciphertext and tag client side.
I have verified that "myHexToken" is the same both client and server side. Also, the return values of the server side "encrypt()" method are correctly sent to the client.

In the WebCrypto code the key must not be hex decoded with fromHex(), but must be converted to an ArrayBuffer with str2ab().
Also, the concatenation of ciphertext and tag must not be converted to an ArrayBuffer with str2ab(), but must be hex decoded with fromHex().
With these fixes decryption works:
Test:
For the test, the following hex encoded key and plaintext are used on the NodeJS side:
const myHexToken = '000102030405060708090a0b0c0d0e0ff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff';
const plaintext = "The quick brown fox jumps over the lazy dog";
const encryptedData = encrypt(plaintext);
console.log(encryptedData);
This results e.g. in the following output:
{
message: 'cc4beae785cda5c9413f49cf9449a6ae17fdc0f7435b9a8fd954602bdb4f4b825793f6b561c0d9a709007c',
tag: '046c8e56bbd13db2faed82d1b19c665e',
iv: '11f87b0eaf006373ae8bc94d'
}
The ciphertext created this way can be successfully decrypted with the fixed JavaScript code:
(async () => {
function fromHex(hexString) {
return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}
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;
}
async function importKey(rawKey) {
var key = await crypto.subtle.importKey(
"raw",
rawKey,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
return key;
}
async function decrypt(iv, data, tag) {
//var rawKey = fromHex(myHexToken.slice(0,32)); // Fix 1
var rawKey = str2ab(myHexToken.slice(0,32));
var iv = fromHex(iv);
//var ciphertext = str2ab(data + tag); // Fix 2
var ciphertext = fromHex(data + tag);
var cryptoKey = await importKey(rawKey)
var decryptedData = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
cryptoKey,
ciphertext
);
var decoder = new TextDecoder();
var plaintext = decoder.decode(decryptedData);
return plaintext;
}
var myHexToken = '000102030405060708090a0b0c0d0e0ff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'
var data = {
message: 'cc4beae785cda5c9413f49cf9449a6ae17fdc0f7435b9a8fd954602bdb4f4b825793f6b561c0d9a709007c',
tag: '046c8e56bbd13db2faed82d1b19c665e',
iv: '11f87b0eaf006373ae8bc94d'
}
var plaintext = await decrypt(data.iv, data.message, data.tag);
console.log(plaintext);
})();
A remark about the key: In the posted NodeJS code, const myHexToken = "0x...." is set. It's not clear to me if the 0x prefix is just supposed to symbolize a hex encoded string, or is really contained in the string. If the latter, it should actually be removed before the implicit UTF-8 encoding (by createCiperiv()). In case of a hex decoding it must be removed anyway.
In the posted example a valid hex encoded 32 bytes key is used (i.e. without 0x prefix).
With regard to the key encoding, also note the following:
The conversion of the key from a hex encoded string by a UTF-8 (or ASCII) encoding results in only half of the key being considered, in the example: 000102030405060708090a0b0c0d0e0f. This reduces security, because the value range per byte is reduced from 256 to 16 values.
In order for the entire key to be considered, the correct conversion on the NodeJS side would be: Buffer.from(myHexToken, 'hex') and on the WebCrypto side: var rawKey = fromHex(myHexToken).
Because of its implicit UTF8 encoding crypto.createCipheriv(..., myHexToken.slice(0,32), ...) creates a 32 bytes key and is functionally identical to str2ab(myHexToken.slice(0,32)) only as long as the characters in the substring myHexToken.slice(0,32) correspond to ASCII characters (which is true for a hex encoded string).

Related

Cloudflare R2 Worker throwing 'Network Connection Lost' error

In my worker I am converting a base64 string I get from the request to a blob with some function. However, when I try to PUT the blob into my bucket, I get "Network Connection Lost" error. I can successfully PUT just the base64 string or any other string but not a blob. Here is my worker:
// Function to convert b64 to blob (working fine)
function b64toBlob(b64Data, contentType, sliceSize=512) {
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: contentType});
return blob;
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case 'PUT':
const contentType = 'application/pdf';
const b64Data = request.body;
const blob = b64toBlob(b64Data, contentType);
try {
await env.qa_sub_agreements_bucket.put(key, blob, { // Failing here
httpMetadata: request.headers,
})
return new Response(blob) // Successfully returns the blob when above PUT is commented out
} catch (e) {
console.error(e.message, e.stack); // Logs out "Error: Network Connection Lost"
}
Hard to say definitively because the Worker posted doesn't appear to be totally complete. An eagle-eyed coworker spotted that it looks like the problem may be that you're invoking atob on a ReadableStream and likely that conversion is what's throwing the exception.

WebCryptoApi: Cannot wrap&unwrap aes-gcm key into&from "jwk" format with "encrypt" and "decrypt" active

I'm generating a key for encrypting data, then wrap it using a master key and store it alongside the encrypted data. All is well when wrapping into raw format, but when wrapping as jwk I get the error DOMException: Data provided to an operation does not meet requirements.
It works when specifying they key for being used either for encryption or for decryption, but not when both are specified as key usages.
let wrapAlgo = {
name: "AES-KW",
length: 256
};
let encAlgo = {
name:"AES-GCM",
length:256
}
let format = "jwk";
let extractable=true;
let keyUsages = ["encrypt", "decrypt"];
let kek = await crypto.subtle.generateKey(
wrapAlgo,
false,
["wrapKey", "unwrapKey"]
);
let key = await window.crypto.subtle.generateKey(
encAlgo,
extractable, // the key is extractable (i.e. can be used in exportKey)
keyUsages
);
console.log("key", key);
let wrappedKey = await crypto.subtle.wrapKey(
format,
key,
kek,
wrapAlgo
);
console.log("wrappedKey", wrappedKey);
let unwrappedKey = await crypto.subtle.unwrapKey(
format,
wrappedKey,
kek,
wrapAlgo,
encAlgo,
extractable,
keyUsages
);
console.log("key", await crypto.subtle.exportKey("jwk", unwrappedKey));
AES-KW is a key wrap algorithm described in RFC3394. The algorithm is used to wrap i. e. encrypt a key. The input, i.e. the key to be encrypted, must be an integer multiple of 8 bytes, s. also here.
The key to be encrypted is passed in SubtleCrypto.wrapKey() in the 2nd parameter key as CryptoKey and must therefore be exported before the actual encryption. For this purpose the format in which the key is exported is specified in the 1st parameter format:
const result = crypto.subtle.wrapKey(format, key, wrappingKey, wrapAlgo);
In the posted example, the key to be wrapped is a 32 bytes key for AES-256. In raw format, the key thus satisfies the AES-KW length criterion. In jwk format, however, the length criterion is generally not met:
If the key exported in jwk format is serialized, it has a length for the key usage ["encrypt"] or ["decrypt"] that happens to be an integer multiple of 8 bytes (112 bytes), while this is not the case for the key usages ["encrypt", "decrypt"] (122 bytes):
(async () => {
async function getLength(keyUsages) {
var key = await window.crypto.subtle.generateKey(
{name:"AES-GCM", length: 256},
true,
keyUsages
);
var expkey = await crypto.subtle.exportKey("jwk", key)
var expkeySerLen = JSON.stringify(expkey).length;
return {KeyUsages: keyUsages, length: expkeySerLen, lenMod8: expkeySerLen % 8};
}
console.log(await getLength(["encrypt"])); // works
console.log(await getLength(["decrypt"])); // works
console.log(await getLength(["encrypt", "decrypt"])); // doesn't work
})();
This is most likely the reason why the code with the key usage ["encrypt"] or ["decrypt"] is executed, but not the code for the key usages ["encrypt", "decrypt"].
The bottom line is that AES-KW works reliably for the raw format, but not for the jwk format.
However, the jwk format can be used in SubtleCrypto.wrapKey() for other wrapping algorithms, such as AES-GCM:
(async () => {
let encAlgo = {
name:"AES-GCM",
length:256
};
let wrapAlgo = {
name:"AES-GCM",
length:256
};
let aesGcmParams = {
name:"AES-GCM",
iv: window.crypto.getRandomValues(new Uint8Array(12))
};
let format = "jwk";
let extractable=true;
let keyUsages = ["encrypt", "decrypt"];
let kek = await crypto.subtle.generateKey(
wrapAlgo,
false,
["wrapKey", "unwrapKey"]
);
let key = await window.crypto.subtle.generateKey(
encAlgo,
extractable, // the key is extractable (i.e. can be used in exportKey)
keyUsages
);
console.log("key (CryptoKey)", key);
console.log("key (jwk)", await crypto.subtle.exportKey("jwk", key));
let wrappedKey = await crypto.subtle.wrapKey(
format,
key,
kek,
aesGcmParams
);
console.log("wrappedKey (ArrayBuffer)", wrappedKey);
let unwrappedKey = await crypto.subtle.unwrapKey(
format,
wrappedKey,
kek,
aesGcmParams,
encAlgo,
extractable,
keyUsages
);
console.log("unwrappedKey (jwk) ", await crypto.subtle.exportKey("jwk", unwrappedKey));
})();

Is it possible to encrypt / decrypt a Sequelize model's field with a unique validation?

for the GDPR I'm trying to encrypt and decrypt my user's personal data from my Sequelize Users model. My username and email fields must be unique.
I tried to use the afterValidate and the beforeValidate hooks, but on validation, Sequelize can't recognize if the fields are unique or not because they already are encrypted, and a crypt is always different.
How do you handle this?
I think you are using random initVector and Securitykey,which will be different in different times. So you have to use a const value which can be saved in a config file.
Ex :
initVectorString: 'xx567876567898765',
securitykeyString:
'Zzzz345678909876545678984567890',
After saving in the config file, you can have a encrypt and decrypt function as follows
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const initVector = Buffer.from(initVectorString, 'hex');
const Securitykey = Buffer.from(securitykeyString, 'hex');
function encryptData(data) {
try {
const cipher = crypto.createCipheriv(algorithm, Securitykey, initVector);
let encryptedData = cipher.update(data, 'utf-8', 'hex');
encryptedData += cipher.final('hex');
return encryptedData;
} catch (error) {}
}
function decryptData(data) {
try {
const decipher = crypto.createDecipheriv(
algorithm,
Securitykey,
initVector
);
let decryptedData = decipher.update(data, 'hex', 'utf-8');
decryptedData += decipher.final('utf8');
return decryptedData;
} catch (error) {}
}

Keep getting Timestamp error on Coinbase API Call

I'm trying to connect my Google Sheet to Coinbase API using apps script. When I try to use the authentication with my keys, I keep getting the same error:
{"errors":[{"id":"authentication_error","message":"invalid timestamp"}]}
(Code 401).
I try to check the time difference between my request and Coinbase server (to see if it is more than 30 seconds) and it doesn't. (1589465439 (mine) / 1589465464 (server)).
My code:
var timestamp = Math.floor(Date.now() / 1000) + 15;
Logger.log(timestamp);
var req = {
method: 'GET',
path: '/v2/accounts',
body: ''
};
var message = timestamp + req.method + req.path + req.body;
var secret = Utilities.base64Decode(apiKey);
secret = Utilities.newBlob(secret).getDataAsString();
//create a hexedecimal encoded SHA256 signature of the message
var hmac = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256, message, secret);
var signature = Utilities.base64Encode(hmac);
Logger.log(signature);
var signatureStr = '';
for (i = 0; i < signature.length; i++) {
var byte = signature[i];
if (byte < 0)
byte += 256;
var byteStr = byte.toString(16);
// Ensure we have 2 chars in our byte, pad with 0
if (byteStr.length == 1) byteStr = '0' + byteStr;
signatureStr += byteStr;
}
Logger.log(signatureStr);
var options = {
baseUrl: 'https://api.coinbase.com/',
url: req.path,
method: req.method,
headers: {
'CB-ACCESS-SIGN': signatureStr,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-KEY': apiKey
}
};
var response = UrlFetchApp.fetch("https://api.coinbase.com/v2/accounts", options);
This is an old question, but in case it's still unsolved, I see a few changes that will fix this. I ran into a similar issue and had to solve it.
You should have 2 keys total (1 API key, 1 secret Key). Secret key is a separate key that comes from Coinbase, and is not a decoded variant of the API access key. It does not need explicit decoding.
Where you're passing timestamp as a header -> convert that value to a string to fix the timestamp error.
headers: {
'CB-ACCESS-SIGN': signatureStr,
'CB-ACCESS-TIMESTAMP': timestamp.toString(),
'CB-ACCESS-KEY': apiKey
}
You can collapse the hmac and signature variables into this one liner before converting to hex.
let signature = Utilities.computeHmacSha256Signature(message, secret);
These 3 changes make your code start to work with my keys.

AWS Cognito token verification c#

I am trying to implement a signature verification endpoint - or ASP.net WebAPI action filter, to verify that a token has in fact come from AWS Cognito - validate its signature.
I am using the following code, but it always returns invalid. The Javascript code example also below works perfectly with the same keys / token.
Can anyone help?
Thanks,
KH
CSharp
public IHttpActionResult Verify([FromBody] string accessToken)
{
string[] parts = accessToken.Split('.');
//From the Cognito JWK set
//{"alg":"RS256","e":"myE","kid":"myKid","kty":"RSA","n":"myN","use":"sig"}]}
var n = Base64UrlDecode("q7ocE2u-JSe1P4AF6_Nasae7e7wUoUxJq058CueDFs9R5fvWQTtAN1rMxBCeLQ7Q8Q0u-vqxr83b6N9ZR5zWUU2stgYzrDTANbIn9zMGDZvSR1tMpun5eAArKW5fcxGFj6klQ0bctlUATSGU5y6xmYoe_U9ycLlPxh5mDluR7V6GbunE1IXJHqcyy-s7dxYdGynTbsLemwmyjDaInGGsM3gMdPAJc29PXozm87ZKY52U7XQN0TMB9Ipwsix443zbE_8WX2mvKjU5yvucFdc4WZdoXN9SGs3HGAeL6Asjc0S6DCruuNiKYj4-MkKh_hlTkH7Rj2CeoV7H3GNS0IOqnQ");
var e = Base64UrlDecode("AQAB");
RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
provider.ImportParameters(new RSAParameters
{
Exponent = new BigInteger(e).ToByteArrayUnsigned(),
Modulus = new BigInteger(n).ToByteArrayUnsigned()
});
SHA512Managed sha512 = new SHA512Managed();
byte[] hash = sha512.ComputeHash(Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(provider);
rsaDeformatter.SetHashAlgorithm(sha512.GetType().FullName);
if (!rsaDeformatter.VerifySignature(hash, Base64UrlDecode(parts[2])))
throw new ApplicationException(string.Format("Invalid signature"));
return Ok(true);
}
// from JWT spec
private static byte[] Base64UrlDecode(string input)
{
var output = input;
output = output.Replace('-', '+'); // 62nd char of encoding
output = output.Replace('_', '/'); // 63rd char of encoding
switch (output.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 1: output += "==="; break; // Three pad chars
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new System.Exception("Illegal base64url string!");
}
var converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;
}
JavaScript
var jwkToPem = require('jwk-to-pem');
var jwt = require('jsonwebtoken');
var jwks = //jwk set file, which you can find at https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json.
//Decode token
var decoded = jwt.decode(token, {complete: true});
//Get the correct key from the jwks based on the kid
var jwk = jwks.keys.filter(function(v) {
return v.kid === decoded.header.kid;
})[0];
//Convert the key to pem
var pem = jwkToPem(jwk);
//Verify the token with the pem
jwt.verify(token, pem, function(err, decoded) {
//if decoded exists, its valid
});
Replace
SHA512Managed sha512 = new SHA512Managed();
by
SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
Don't forget to set properly the hash algorithm properly as well
rsaDeformatter.SetHashAlgorithm("SHA256");
Flo's answer works but its built into .net now. Using https://rafpe.ninja/2017/07/30/net-core-jwt-authentication-using-aws-cognito-user-pool/ which has more details on how to build it into the .net core middleware:
public RsaSecurityKey SigningKey(string Key, string Expo)
{
return new RsaSecurityKey(new RSAParameters()
{
Modulus = Base64UrlEncoder.DecodeBytes(Key),
Exponent = Base64UrlEncoder.DecodeBytes(Expo)
});
}
public TokenValidationParameters TokenValidationParameters()
{
// Basic settings - signing key to validate with, audience and issuer.
return new TokenValidationParameters
{
// Basic settings - signing key to validate with, IssuerSigningKey and issuer.
IssuerSigningKey = this.SigningKey(CognitoConstants.key,CognitoConstants.expo),
ValidIssuer = CognitoConstants.Issuer,
ValidAudience = CognitoConstants.clientid,//Same value you send in the cognito request url
// when receiving a token, check that the signing key
ValidateIssuerSigningKey = true,
// When receiving a token, check that we've signed it.
ValidateIssuer = true,
// When receiving a token, check that it is still valid.
ValidateLifetime = true,
// Do not validate Audience on the "access" token since Cognito does not supply it but it is on the "id"
ValidateAudience = true,
// This defines the maximum allowable clock skew - i.e. provides a tolerance on the token expiry time
// when validating the lifetime. As we're creating the tokens locally and validating them on the same
// machines which should have synchronised time, this can be set to zero. Where external tokens are
// used, some leeway here could be useful.
ClockSkew = TimeSpan.FromMinutes(0)
};
}
private bool ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.CanReadToken(token))
{
var validationParams = TokenValidationParameters();
SecurityToken validatedToken;
//ValidateToken throws if it fails so if you want to return false this needs changing
var principal = tokenHandler.ValidateToken(token, validationParams, out validatedToken);
return validatedToken != null;
}
return false;
}