This is a 4 year old nodejs project I took over, and I was asked to refactor it using golang, but in the refactoring I found that the nodejs encryption was deprecated. And, I don't know which mode of AES is used to encrypt this code
Can any expert help me to see how to decrypt this nodejs encryption with golang? Thank you very much!
Encryption code for nodejs :
exports.createToken = function (src: string, timestamp: string, key: any) {
var msg = src + '|' + timestamp;
var cipher: any = CryptoJS.createCipher('aes256', key);
var enc: any = cipher.update(msg, 'utf8', 'hex');
enc += cipher.final('hex');
return enc;
};
Decryption code for nodejs :
exports.parseToken = function (token: string, key: string): any {
let decipher = CryptoJS.createDecipher('aes256', key);
let dec: string;
try {
dec = decipher.update(token, 'hex', 'utf8');
dec += decipher.final('utf8');
} catch (err) {
console.error('[token] fail to decrypt token. %j', token);
return null;
}
var ts = dec.split('|');
if (ts.length !== 2) {
// illegal token
return null;
}
return { src: ts[0], timestamp: Number(ts[1]) };
};
The deprecated methods crypto.createCipher() and crypto.createDecipher() apply the proprietary OpenSSL function EVP_BytesToKey() to derive a 32 bytes key and a 16 bytes IV from a password. No salt is used, the digest MD5 and an iteration count of 1. This algorithm is very insecure, which is why both methods are deprecated.
The posted code applies aes-256-cbc (i.e. AES-256 in CBC mode) and just this key derivation to derive a key/IV pair. Since the key derivation does not use a salt, always the same ciphertext results for the same plaintext and password. E.g. for:
var src = 'The quick brown fox jumps over the lazy dog';
var timestamp = '1616409134831';
var passphrase = 'my secret passphrase';
the ciphertext is:
60673700fb64da36b65829ee3c578d1ec675638a95c8dee4e7c026ee72a837c2170c13b7b24125c02871663a64fd646dd9994793943eeb70b3e959cbc4cd423a
So for decryption in Go you need an implementation of EVP_BytesToKey(), e.g. here:
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"github.com/walkert/go-evp"
"encoding/hex"
)
func main() {
key, iv := evp.BytesToKeyAES256CBCMD5([]byte(""), []byte("my secret passphrase")) // MD5, no salt, passprase: my secret passphrase
ciphertext, _ := hex.DecodeString("60673700fb64da36b65829ee3c578d1ec675638a95c8dee4e7c026ee72a837c2170c13b7b24125c02871663a64fd646dd9994793943eeb70b3e959cbc4cd423a")
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
cbc := cipher.NewCBCDecrypter(block, iv)
plaintextPadded := make([]byte, len(ciphertext))
cbc.CryptBlocks(plaintextPadded , ciphertext)
plaintext := string(PKCS7Unpad(plaintextPadded ))
fmt.Println("Decrypted data: ", string(plaintext))
}
func PKCS7Unpad(src []byte) []byte { // PKCS7 unpadding from https://stackoverflow.com/a/41595640/9014097
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
Running this code results in the above plaintext.
Related
I'm trying to decrypt a string that was encrypted with SubtleCrypto AES-GCM in the browser (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt). Using the following code in Javascript:
async function aesGcmEncrypt(plaintext, password) {
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv
const ivStr = Array.from(iv).map(b => String.fromCharCode(b)).join(''); // iv as utf-8 string
const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw
const ptUint8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8
const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8); // encrypt plaintext using key
const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array
const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string
return btoa(ivStr) + "." + btoa(ctStr); // iv+ciphertext base64-encoded
};
And im trying to decrypt the string in Go using:
func Decrypt(encryptedString string, keyString string) string {
split := strings.Split(encryptedString, ".")
key := []byte(keyString)
enc, err := base64.StdEncoding.DecodeString(split[1])
if err != nil {
panic(err.Error())
}
nonce, err := base64.StdEncoding.DecodeString(split[0])
if err != nil {
panic(err.Error())
}
//Create a new Cipher Block from the key
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
//Create a new GCM
aesGCM, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
//Decrypt the data
plaintext, err := aesGCM.Open(nil, nonce, enc, nil)
if err != nil {
panic(err.Error())
}
return string(plaintext)
}
However, every time I run into: "cipher: message authentication failed".
I guess there's something different with the implementations but I can't seem figure out what.
I tried using HEX instead of base64 (encoding and decoding) and tried using "cipher.NewGCMWithNonceSize()" instead of "cipher.NewGCM(block)".
I want to decrypt a message on the client-side(react.js) using Web Crypto API which is encrypted on the back-end(node.js), however I ran into a weird problem and don't have any idea what is wrong(I also checked this)
node.js
function encrypt(message){
const KEY = crypto.randomBytes(32)
const IV = crypto.randomBytes(16)
const ALGORITHM = 'aes-256-gcm';
const cipher = crypto.createCipheriv(ALGORITHM, KEY, IV);
let encrypted = cipher.update(message, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag()
let output = {
encrypted,
KEY: KEY.toString('hex'),
IV: KEY.toString('hex'),
TAG: tag.toString('hex'),
}
return output;
}
react.js
function decrypt() {
let KEY = hexStringToArrayBuffer(data.KEY);
let IV = hexStringToArrayBuffer(data.IV);
let encrypted = hexStringToArrayBuffer(data.encrypted);
let TAG = hexStringToArrayBuffer(data.TAG);
window.crypto.subtle.importKey('raw', KEY, 'AES-GCM', true, ['decrypt']).then((importedKey)=>{
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: IV,
},
importedKey,
encrypted
).then((plaintext)=>{
console.log('plainText: ', plaintext);
})
})
function hexStringToArrayBuffer(hexString) {
hexString = hexString.replace(/^0x/, '');
if (hexString.length % 2 != 0) {
console.log('WARNING: expecting an even number of characters in the hexString');
}
var bad = hexString.match(/[G-Z\s]/i);
if (bad) {
console.log('WARNING: found non-hex characters', bad);
}
var pairs = hexString.match(/[\dA-F]{2}/gi);
var integers = pairs.map(function(s) {
return parseInt(s, 16);
});
var array = new Uint8Array(integers);
return array.buffer;
}
Encryption in back-end is done without any error, however when want to decrypt the message on the client-side, the browser(chrome) gives this error: DOMException: The provided data is too small and when I run the program on firefox browser it gives me this error: DOMException: The operation failed for an operation-specific reason. It's so unclear!!
By the way what's the usage of athentication tag in AES-GCM is it necessary for decryption on the client-side?
GCM is authenticated encryption. The authentication tag is required for decryption. It is used to check the authenticity of the ciphertext and only when this is confirmed decryption is performed.
Since the tag is not applied in your WebCrypto Code, authentication and therefore decryption fail.
WebCrypto expects that the tag is appended to the ciphertext: ciphertext | tag.
The data in the code below was created using your NodeJS code (please note that there is a bug in the NodeJS code: instead of the IV, the key is stored in output):
decrypt();
function decrypt() {
let KEY = hexStringToArrayBuffer('684aa9b1bb4630f802c5c0dd1428403a2224c98126c1892bec0de00b65cc42ba');
let IV = hexStringToArrayBuffer('775a446e052b185c05716dd1955343bb');
let encryptedHex = 'a196a7426a9b1ee64c2258c1575702cf66999a9c42290a77ab2ff30037e5901243170fd19c0092eed4f1f8';
let TAGHex = '14c03526e18502e4c963f6055ec1e9c0';
let encrypted = hexStringToArrayBuffer(encryptedHex + TAGHex)
window.crypto.subtle.importKey(
'raw',
KEY,
'AES-GCM',
true,
['decrypt']
).then((importedKey)=> {
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: IV,
},
importedKey,
encrypted
).then((plaintext)=>{
console.log('plainText: ', ab2str(plaintext));
});
});
}
function hexStringToArrayBuffer(hexString) {
hexString = hexString.replace(/^0x/, '');
if (hexString.length % 2 != 0) {
console.log('WARNING: expecting an even number of characters in the hexString');
}
var bad = hexString.match(/[G-Z\s]/i);
if (bad) {
console.log('WARNING: found non-hex characters', bad);
}
var pairs = hexString.match(/[\dA-F]{2}/gi);
var integers = pairs.map(function(s) {
return parseInt(s, 16);
});
var array = new Uint8Array(integers);
return array.buffer;
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
Encryption strategy:
Generate random 256-bit encryption key (K_s).
For every PII value in payload:
1. Pad plaintext with PKCS#7 padding.
2. Generate random 128-bit Initialization Vector (IV).
3. Encrypt padded plaintext with AES-256-CBC Cipher generated with key K_s and IV to get ciphertext.
4. Append IV to cipher text and Base64 encode to get payload value.
5. Assign payload value to corresponding key in payload.
6. Encrypt K_s using RSA-OAEP with hash function SHA-256 and public RSA key to get K_enc.
7. Assign K_enc to session_key in payload.
I'm trying to implement the above encryption strategy in node js using crypto module, but I'm missing something... I'm stuck on this on the past 2 days... Can someone please help me figure out what I'm missing here?
My implementation of encryption script so far below:
const crypto = require('crypto'),
_ = require('lodash');
async function encryptPayload(dataToEncrypt, password) {
if (dataToEncrypt.constructor !== String) {
dataToEncrypt = JSON.stringify(dataToEncrypt);
}
let bufferKey = Buffer.from(password, 'hex');
const iv = crypto.randomBytes(16); // should this be crypto.randomBytes(32).toString('hex')?
let cipherKey = crypto.createCipheriv('aes-256-cbc', bufferKey, iv);
cipherKey.setAutoPadding(true);
let encryptedPayload = cipherKey.update(dataToEncrypt, 'utf8', 'base64');
// encryptedPayload += cipherKey.final('base64');
// return encryptedPayload + iv.toString('base64');
encryptedPayload = cipherKey.final()
let tempBuffer = Buffer.concat([encryptedPayload, iv]);
return tempBuffer.toString('base64');
}
async function encryptDataMultipleKeys(payload, publicKey, keysToEncrypt = []) {
if (!payload) {
return payload;
}
let password = crypto.randomBytes(32).toString('hex'); //uuid.v4();
console.log("The password is " + password + " \n");
let pendingPromisesArray = [], correspondingKeyNameArray = [];
for (const key of keysToEncrypt) {
let value = _.get(payload, key);
if (!value) {
continue;
}
//value = await encryptPayload(value, password);
pendingPromisesArray.push(encryptPayload(value, password));
correspondingKeyNameArray.push(key);
}
let promisesValueArray = await Promise.all(pendingPromisesArray);
let encryptedPayload = {}
for (let index = 0; index < correspondingKeyNameArray.length; index++) {
let key = correspondingKeyNameArray[index];
let value = promisesValueArray[index];
if (!value || !key) {
continue;
}
_.set(encryptedPayload, key, value);
//encryptedPayload[key] = value;
}
//REF: https://nodejs.org/api/crypto.html#crypto_crypto_publicencrypt_key_buffer
let encryptedPasswordBuffer = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}, Buffer.from(password, 'hex'));
let encryptedPassword = encryptedPasswordBuffer.toString('base64');
encryptedPayload.session_key = encryptedPassword
return encryptedPayload;
}
async function encryptPIIFields(payload) {
let fieldsToEncrypt = [
'applicant.ssn', 'applicant.date_of_birth', 'applicant.first_name', 'applicant.last_name',
'applicant.email_address', 'applicant.phone_number', 'applicant.income',
'applicant.address.line_1', 'applicant.address.line_2', 'applicant.address.city',
'applicant.address.country', 'applicant.address.state', 'applicant.address.zipcode'
];
let publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYsdy+gGrdzvG5F9BYLl\nVwFwCfyCzeLQ7Vmvu+wvyoDrwvMXSfLnZfg7NsZMyPc3OVt8EeRvGLzrXvxtSWKG\n+mKBC7xEzb/LM8MoHQhXlgZ7L1nofBpAs74zEFXZNGHw5SnWXTuQ3Yym0u8hkYDZ\noqDJRgrczjXdbrqDVeB3GIvpMZMU9OkTFRmZZGMLVS3P3LIswyxfdxuMvU9dBBtP\nj3wofaLuxNWA384xBZYNV7AcWzOOHR3j3Iw7KfplgVawlpm4zXhBwFrKE44g0g5z\n4vL2N1eJs/OgaAMUYUM4kuZIW1fqFGB9cRAJpbjCO9d3dnvz4sPBWXchzZVjyzXh\njwIDAQAB\n-----END PUBLIC KEY-----\n";
payload = await encryptDataMultipleKeys(payload, publicKey, fieldsToEncrypt);
return payload
}
let data = {
"applicant": {
"address": {
"line_1": "732484THSTREETss",
"city": "TACOMA",
"country": "US",
"state": "WA",
"zipcode": "98498"
},
"income": 1000,
"date_of_birth": "1938-09-09",
"email_address": "faa4#mail.com",
"first_name": "WILLIAM",
"last_name": "SCALICI",
"phone_number": "7327474747",
"ssn": "987452343"
}
}
encryptPIIFields(data).then((encryptedData) => {
console.log(JSON.stringify(encryptedData)); //eslint-disable-line
process.exit(0);
}, (err) => {
console.log(err); //eslint-disable-line
process.exit(1);
});
Decryption script:
const crypto = require('crypto'),
_ = require('lodash');
async function decryptDataMultipleKeys(payload, privateKey, keysToDecrypt) {
if (!payload) {
return payload;
}
let decryptedPasswordBuffer = crypto.privateDecrypt({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}, Buffer.from(payload.session_key, 'base64'));
let password = decryptedPasswordBuffer.toString('hex');
console.log("password: " + password);
let decryptedPayload = {};
for (const key of keysToDecrypt) {
let value = _.get(payload, key);
if (!value) {
continue;
}
let encryptedDataBuffer = Buffer.from(value, 'base64');
let bufferData = encryptedDataBuffer.slice(0, 16);
let bufferIv = encryptedDataBuffer.slice(16, 32);
let cipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(password, 'hex'), bufferIv);
cipher.setAutoPadding(true);
let decryptedValue = cipher.update(bufferData, undefined, 'utf8');
decryptedValue += cipher.final('utf8');
_.set(decryptedPayload, key, decryptedValue);
}
return decryptedPayload;
}
async function decryptPIIFields(payload) {
let fieldsToDecrypt = [
'applicant.ssn', 'applicant.date_of_birth', 'applicant.first_name', 'applicant.last_name',
'applicant.email_address', 'applicant.phone_number', 'applicant.income',
'applicant.address.line_1', 'applicant.address.line_2', 'applicant.address.city',
'applicant.address.country', 'applicant.address.state', 'applicant.address.zipcode'
];
let privateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEArYsdy+gGrdzvG5F9BYLlVwFwCfyCzeLQ7Vmvu+wvyoDrwvMX\nSfLnZfg7NsZMyPc3OVt8EeRvGLzrXvxtSWKG+mKBC7xEzb/LM8MoHQhXlgZ7L1no\nfBpAs74zEFXZNGHw5SnWXTuQ3Yym0u8hkYDZoqDJRgrczjXdbrqDVeB3GIvpMZMU\n9OkTFRmZZGMLVS3P3LIswyxfdxuMvU9dBBtPj3wofaLuxNWA384xBZYNV7AcWzOO\nHR3j3Iw7KfplgVawlpm4zXhBwFrKE44g0g5z4vL2N1eJs/OgaAMUYUM4kuZIW1fq\nFGB9cRAJpbjCO9d3dnvz4sPBWXchzZVjyzXhjwIDAQABAoIBAQCBNy03bwrSF8fd\nUgWxvdW/Y62lceN/IxwHLhlAJksrT7S7kj7L69XJwfts/Fed5xyyU2Dc/aaO19O1\nBOTmmDsCYafOMh9UxzKo1u2eOGDmruq3xgzpoq58Zukkh5dTfn1cVDttbfWeUKTC\nOBVZfoQNqARVZ68ix06ZrLwvjBOBLSmH4l4XM8JzYtBFOntkU45ZHmPvxGfJBvYS\nhTOMvS3AvfxuEK2zW9A/vciDWVWmET0p0C22+pMahT+FSwOwYNTuP3BxQV2Aq6vY\nEc9ktr4hj0b2gGoRok/t4K4C/ufDhxRinNnFIFcPh9j39/st8kLwlkKCgii3Kpjv\ntzD4OyX5AoGBANwB77oOmbIGNdXGONTQ1aXnqpsO0tt1/ZAnZrQaNgCb6ThwLieN\nQ5tqem6GWbTtSSUuwpgFjxw5SMD8KxJihV+ySjo99SGhqssyPXyYHpMmOSEsbQhe\n0YeT4Epr6FuIBLuV0qFZJupI6jcHBKcmR0FQ2rXqCxPnfNopZizm5GnbAoGBAMnv\nOxIdpI2r2Z/+6WyQiBmwuEhd39ZKA8aoONJeoCp0MnAQvrbmr6kDfpP+aQWw6Xww\n+5GrAFgrtJ37STHPXw/lXPKDpXE573o8aDHTDB/WU0lpCVxJ6NY0sy/CArUIU7Pz\ntQiB11PrZZ6UDyiSmXoYzUHkR1I44EjF2/lnZlddAoGBALvx44s8Qcw1RfQzfAVB\nyeIKwFHqHfNhHpXxMumUoqFuj5OpMaSUJzczhRe6KhRHyP68rXwU86aWwTIrudfg\n1jNkKckLeMecRj2D08cGZMgsFQ3j19kYt0Js72RkPoFC91gQq3kuofHvDDaqBi2M\no76GhfB12bTNQnlUeHbPYD2VAoGALZ7kg4U65d7LPcBDUAmfFd6842yB41G5ZKog\nnDZQjQbPVk4SKBQZ318wu5Kge26qcSpHy3MMkt7c4UwiDyTAX0D8LLXdLKVgGweG\nqqr5dD/hdRZLzRPNjIc/bCyym9+TuXX3kkJzOTxXKupcOlhUYCc2SAqgqky7LvW0\narYXgukCgYEAjtfYSciex+Nv1GGaN7SjAozIBvrLAV0o9oo/zxhTblJpCkaM60aT\nimiT4NwkrEfB27zzguYduD0mgsq/Hg8BBkbe7FPKZ8GugZ6xlF0i02kVRzRDNlxT\n+cfqbL2vKt5FR9iFJFVWYjmvpVmvxZ/J1ybZD3MjT+YBNj/sf9DvclM=\n-----END RSA PRIVATE KEY-----\n";
payload = await decryptDataMultipleKeys(payload, privateKey, fieldsToDecrypt);
return payload
}
let data = {"applicant":{"ssn":"YR8BUBk+xrpQm5gHkCfrIXMFGjGJGLS192mVgcupF6U=","date_of_birth":"+ujL7mv/IZMALdFiL92Z0LACrVhb/lmzcwx8l89sIcs=","first_name":"l8nAmcQkIm8OctcaFq9t4q5TN2brkf4MTfdQ7K19PMw=","last_name":"yOqZpZjueZu10q0z3P4cTN2m5BP7ug4CqypumfzjbUc=","email_address":"2CftSOnWqRCINRF9ZK5QYTSP6TdpTUEpEanJE6PAhUQ=","phone_number":"cEQV5cbYJveBkn3XWqzCw2x9a8P2ZcEjiMX5+ezhdQc=","income":"TpM/4zOiTpCZ8to8jjjngJDLRcrDKOP8C2UVRYh9Wgs=","address":{"line_1":"MYzvsUFBl+Oav1aDOxqvjimpv8YW4g2hSjZChfOeri4=","city":"/3m9bvk1auwNgyNTJ2gtx1B0+gYxKQYy/VBThyuqrr0=","country":"H8GZ9rP+EAw9KdeVvNbPFtPyUBtU9NrCxXrQ0GMTltg=","state":"g7nshQ6rNrbsPq1vJd5vnBh/0HNjasfgN8Mhy59FW/U=","zipcode":"X5MGNTPA/Rh2Fxb8GOLUBwHx9ex8RGGrRM+RA7Wf8MU="}},"session_key":"CDfUI+12UzezVpp/7/9jbWXJ7AmR5jTcV5r9JsyIPinxZO2nEra05t8uL3lOotyE23ymr1e3Ia8mF7huReIbTma25I7p01+eBjKBR9Zv5NHV72is44wmJqXu5dB1fOiJFF7xBjUzZ5zClgBMsFNr025yc4dtDKQxPcj0xGPvQKmUbbbwTvq7TrSS0rDZrjcGLsxlpIXua1damYp+n6Jw9XjLyN4OTyiV2JtiOq7vnRMEYsdTr4hibVhtFwkDFqCrg7Y9tnvgLocg2lMwEOu/iF7QDA5UlAUyiFU+U0WThasVjPCNikoRi2FC2u/T/EAtmG9drWuohxX2DUvyKgm/bA=="}
decryptPIIFields(data).then((decryptedData) => {
console.log(JSON.stringify(decryptedData)); //eslint-disable-line
process.exit(0);
}, (err) => {
console.log(err); //eslint-disable-line
process.exit(1);
});
I have a feeling that I'm messing something in the part where I append the IV to the encrypted payload... Need some enlightenment here.
EDIT: I have added a script to decrypt the same. I'm unable to successfully decrypt only certain cases.
For example, I can decrypt if the value of line_1 is "732484THSTREETs", but can't decrypt if the value is "732484THSTREETss"... I'm getting the following the error while decrypting the latter
Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
at Decipheriv.final (internal/crypto/cipher.js:172:29)
at decryptDataMultipleKeys (/Users/pavithran/off/payment-service/oaep-decrypt.js:29:30)
at decryptPIIFields (/Users/pavithran/off/payment-service/oaep-decrypt.js:43:19)
at Object.<anonymous> (/Users/pavithran/off/payment-service/oaep-decrypt.js:48:1)
at Module._compile (internal/modules/cjs/loader.js:1158:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
at Module.load (internal/modules/cjs/loader.js:1002:32)
at Function.Module._load (internal/modules/cjs/loader.js:901:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
at internal/main/run_main_module.js:18:47 {
library: 'digital envelope routines',
function: 'EVP_DecryptFinal_ex',
reason: 'bad decrypt',
code: 'ERR_OSSL_EVP_BAD_DECRYPT'
}
The problem is in both the symmetric encryption (wrong usage of update and final) and the symmetric decryption (wrong separation of the ciphertext). In detail the current version does not work for the following reasons:
In the symmetric encryption only the final part is considered. But of course the preceding update statement must be considered as well, which has to be stored in a Buffer for the subsequent concatenation, i.e. the third argument (the encoding) must be removed. Furthermore the IV is usually placed before (and not after) the ciphertext. The latter is not a bug, but it is still useful to follow conventions. All in all, therefore, for symmetric encryption it must be:
...
let encryptedPayloadUpd = cipherKey.update(dataToEncrypt, 'utf8'); // store in buffer
let encryptedPayloadFin = cipherKey.final()
let tempBuffer = Buffer.concat([iv, encryptedPayloadUpd, encryptedPayloadFin]); // consider encryptedPayloadUpd, place IV before ciphertext
return tempBuffer.toString('base64');
...
which produces e.g. the following ciphertext:
{"applicant":{"ssn":"zFbx9fiBSu47bMiAP7whaG+fkOBrCu+CWBzfYjPcV14=","date_of_birth":"K/GzpKNIDY4Bb0MJpNfvv/wE3iUBP31y5OS1t8LTEJg=","first_name":"HbVtwcy4wVV5n7JLpt87IhX27JiLn9ewaqj08EXw8Ss=","last_name":"D5lqNNYywt88MOSlMcZQY6oTLuntTYzFvOy1op7PhjY=","email_address":"hNBSep2jzczUiBm0M7iGTZcPo3GZVScOgKzjd+t3uYA=","phone_number":"0l4PgCW12WFb1jv9lfOftHngQlE8BWsbqi/HHdcmjhk=","income":"nu16KkULL/xyBgKQjxAn//Q34fdA0kAOMS+AJTYXh4k=","address":{"line_1":"ce2BBt+Qbpe8KpJR81zaqQh7CSF3WXni6snLYZYGPuHknR3qBCY2fLdKvgMl8D2E","city":"01eVK0h7zGOSnL8I4aQ+CICSQV1t7bU470/S1HY5ZmY=","country":"XHjNTEc8ZapnuBSgLgg2YIZ9fIc7m8hH/j/nULL1UZo=","state":"17m0tTQQaT8c4y+XXVQsz8tfjIDGrOh2tBMTAcH+5PY=","zipcode":"ygjxgvF3B0HAnvtpys5s7bDMABvg6IcJDKJAIMNuLjk="}},"session_key":"jEqblsQ5ZbGDmZBlzZgXZWAxtQptL+9FL2WKvMQHL5PdTDwez1XKMl6aAKHRoMjb3oH0GDw941ICGL99WHW+nxJaanxqV9mlU9NDBE84T/fdrov/YAS5NDb5CD20ZFT8YL+/QC3ldf4VvJlzLy18EvSgt1nPYUZ6WEfdpNs6YckxtV4NAQ1wNiB/zQ07RUUfIegdNE9vn828TjOqxTUDKkwtZiyKKtaIetWS9LnCSDh7PXEnWyAcHZ19WRTZimvoMuqPUjotChzCjNrwTEkoOp/XzPN3NhG/7nxxw9vFNSP0Gy6jPHXUBiJ9sMPkg99TZCk9+2hWGdMiuP4JHpvk4g=="}
For the symmetric decryption it is assumed that the ciphertext is only one block (16 bytes for AES) large, which is generally not true. Any plaintext consisting of more than 1 block will generate a larger ciphertext (even a 1 block plaintext generates a 2 block ciphertext because of the PKCS7 padding used). For the symmetric decryption (with the order IV, ciphertext) it must therefore be:
...
let encryptedDataBuffer = Buffer.from(value, 'base64');
let bufferIv = encryptedDataBuffer.slice(0, 16); // consider order (IV, ciphertext)
let bufferData = encryptedDataBuffer.slice(16); // consider complete ciphertext
...
With this the above ciphertext can be decrypted:
{"applicant":{"ssn":"987452343","date_of_birth":"1938-09-09","first_name":"WILLIAM","last_name":"SCALICI","email_address":"faa4#mail.com","phone_number":"7327474747","income":"1000","address":{"line_1":"732484THSTREETss","city":"TACOMA","country":"US","state":"WA","zipcode":"98498"}}}
Please note: The encryption and Base64 encoding in encryptPayload of the posted code in the question has been changed relative to the original post. Before the change ciphertext and IV were each Base64 encoded and then concatenated. This is unusual, as Base64 encoding generally occurs after concatenation. But this is not a bug as long as the decryption is implemented consistently. In contrast, the code after the change did not work, as explained in detail above. The posted code snippets in this answer implement the usual scheme: concatenation of IV and ciphertext in this order, followed by Base64 encoding.
Before I get too much into the details, the high level thing I'm trying to accomplish is encrypting some data in JavaScript, sending that to a web server, then decrypting that encrypted data in C#. The part I'm having trouble with is decrypting the data in C#.
I'm encrypting some data in JavaScript like this (I removed the extraneous code):
// https://github.com/diafygi/webcrypto-examples#rsa-oaep---encrypt
window.crypto.subtle.encrypt(
{
name: "RSA-OAEP"
},
publicKey,
data
)
.then(function (encrypted) {
// ...
});
I confirmed that I can decrypt it in JavaScript like so (note that I don't actually want to do this, but I did it to prove that the data could be decrypted):
function decryptValue () {
// Base64 decode the encrypted data for the value "Bob".
var data = base64Decode("CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==");
// Get private key.
var keyPromise = importPrivateKey();
return keyPromise.then(function (privateKey) {
// Decrypt the value.
return window.crypto.subtle.decrypt(
{
name: "RSA-OAEP"
},
privateKey,
data
)
.then(function (decrypted) {
// Log the decrypted value to the console.
console.log(arrayBufferToString(decrypted));
});
});
}
For simplicity, that code sample is decrypting a previously encrypted value of "Bob". This works fine.
The problem occurs when I try to decrypt the value in C#:
public static string Decrypt()
{
// The encrypted and base64 encoded value for "Bob".
var encryptedValue = "CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==";
// Assuming RSA-OAEP.
var doOaep = true;
// Setup encryption algorithm.
var provider = GetPrivateKey();
// Decrypt value.
var encryptedData = Convert.FromBase64String(encryptedValue);
// This line throws an error: "Error occurred while decoding OAEP padding."
var decryptedData = provider.Decrypt(encryptedData, doOaep);
var decryptedText = Encoding.Unicode.GetString(decryptedData);
// Return decrypted text.
return decryptedText;
}
The line that says provider.Decrypt(encryptedData, doOaep) throws an error with a message of "Error occurred while decoding OAEP padding." The stack trace is:
Error occurred while decoding OAEP padding.
at System.Security.Cryptography.RSACryptoServiceProvider.DecryptKey(SafeKeyHandle pKeyContext, Byte[] pbEncryptedKey, Int32 cbEncryptedKey, Boolean fOAEP, ObjectHandleOnStack ohRetDecryptedKey)
at System.Security.Cryptography.RSACryptoServiceProvider.Decrypt(Byte[] rgb, Boolean fOAEP)
It seems like maybe the way the JavaScript is encrypting the value is not compatible with the way the C# is encrypting the value. Before I completely abandon this approach and try another JavaScript library for encryption, is there some way around this error?
For additional context, I am guessing this error is related to something mentioned in this article: https://www.codeproject.com/Articles/11479/RSA-Interoperability-between-JavaScript-and-RSACry
It says:
Incompatible padding scheme from the JavaScript code would produce the
"bad data" exception at the server side.
The JavaScript code therefore needs to implement one of two padding
schemes used in the .NET RSA implementation, the first is PKCS#1 v1.5
padding and another is OAEP (PKCS#1 v2) padding.
I'm not getting that exact exception, but maybe since that article was written the error message has changed. In any event, what that article says seems to imply that the way the JavaScript is encrypting isn't compatible with the way the C# is decrypting (namely, due to C#'s requirement for padding).
Is there something I'm missing? Is there some parameter or some easy way to get encryption working in JavaScript and decryption working in C#? Perhaps there is some C# library that decrypts in a way that is compatible with the way the JavaScript is encrypting?
Here's a full example that shows the JavaScript is decrypting properly (only works on some browsers... probably not going to work on IE):
function decryptValue () {
// Base64 decode the encrypted data for the value "Bob".
var data = base64Decode("CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==");
// Get private key.
var keyPromise = importPrivateKey();
return keyPromise.then(function (privateKey) {
// Decrypt the value.
return window.crypto.subtle.decrypt(
{
name: "RSA-OAEP"
},
privateKey,
data
)
.then(function (decrypted) {
// Log the decrypted value to the console.
console.log("Decrypted value: " + arrayBufferToString(decrypted));
});
});
}
function importPrivateKey() {
var rawKey = {
"alg": "RSA-OAEP-256",
"d": "E4KDwgxy7jFrqeXqKjxPTGOdbEoZ2aWj5qcZhUJcnr9Qh_jg_grkgpHVwEbQifTxsipXTiR3_ygspI4XFoeV-wDVfWqWCVR3_bHChF9PW8Ak1x_dBSS28BMs8PdthI1pDbpqPhmMcF4riHCtNo1M1v8cLdeaiqiXitNVBkaTePsDiucfwOy1rgxwBqAL1CNJhP8oRiYkxD-gfE_EapWuXY9-wF9O-lXPLSTKWgMmmVxSmhUP-Uqk7cJ24UH9C7W7hnSQU4pkfD5XHx3_2WO2GMKKZcqz39wJUrQzrIO7539SYsQ3rEe4aMJyL4U-Ib4_purzVS0DRjzGxK8chT2guQ",
"dp": "kibhWHk1R6yBlhZbjIrNl9beAkyV5vtFsj_F0ixbIITzjSqI_td71sWjKQvJ2rR7hu5DYTZ4p3XwBeQ2jpYQV-y5uh4v7rGngh-0GHuHqMiUQnejgYGcHgng4iCM4e3aTO7QUlP8jqRfxw6xpfNTjrVbAL8LtdCG21vmqOiLkXE",
"dq": "qLF9x-zKfaXlLsNgBQ1ZnaQexrnJRqrRh9JSU85fCNy5mmpKWAUbCHB-59CGAId8wMAnAyEpjcBOKNTqWSlNzp84xeUHcyPI-Dt4Yp_Y_dXjGAYntALSJs4qeF2rk55MSpiSD_KSU4DknX_E_G2rFMY7AZOSwi1D8YcNmj5okTE",
"e": "AQAB",
"ext": true,
"key_ops": [
"decrypt"
],
"kty": "RSA",
"n": "oQeTwOlTc6rIb2kddwIOc0Ywslc7YzJSRZd_PegW7T3nO3DqCI5kp5EJmnGP8JJ9sbyVYyAHFLZQtMP69UspZFn__fBk2LTp2QdqBSMHbObENcSiG2FH-pZSwCaj3Pvy-qvTjnkxxN-3OE6oB8EcX5ekZwCZzAxazbVXctY_hCcaTWG7ugwc_ZyvhsdE7wa3pnTfXYHWXcDDT8FTpYl62aqWsEIUAJSkgmQ9zce0RiDUjBJyJEM9P0ihp1Ab8BD88pEM22-PXfiOesRzp5yOsjzI3kdr5KPsshstneJEGHYae5GZXLUpnVMRY1TCFFLbkPwK6oVkRaVU1RvK9ssO3Q",
"p": "2TTEToB4AuPIPPpg3yTyBlGb_m-f4r-TxpU96ConV2p696_4QI6jlPWwgcC9Vdma_Da43AGuyLzIptgkzF8nSjV80VwwDKQ1YkFPc6ZqB2isvExuieSP6_jLlB-fCyCLqtTxpPm2VcK16Pqm0s5T0QGH6cQjjm1r2Ww1wuaiQbk",
"q": "vcpFwkZKZ3hx3FpHgy3ScuuTRSPO2ge8TE8UMJdCrEnpftAeYuVYrJqnxfzKgyl02OijAUi1eozJxj_lM5McxrKZEEAvo6e8wtzl2hnkUh-KWoBJ8ii0VJcu6U5vs4pcv-lYBPFC6fzoGnUw8LNWMxb5ejgYbLUWp10BbfkWGEU",
"qi": "Mza7JYleki7BvmD3dX5CO2nkD3mBGz4_0P_aoWyHEkWu4p5XWillaRVWyLnQEubLvAduUCr-lhfNmzdUhHecpE438_LQNtKRyOq9zkvjsMOGDmbkKpZ7-aTSshax6KNlYOWdOkadjuLtRExCmwbzu5lgI4NwacxSs5MfjHMrTCo"
};
return window.crypto.subtle.importKey(
"jwk",
rawKey,
{
name: "RSA-OAEP",
hash: { name: "SHA-256" }
},
true,
["decrypt"]
);
}
function arrayBufferToString(buffer) {
var result = '';
var bytes = new Uint8Array(buffer);
for (var i = 0; i < bytes.length; i++) {
result += String.fromCharCode(bytes[i]);
}
return result;
}
// Decodes a base64 encoded string into an ArrayBuffer.
// https://stackoverflow.com/a/36378903/2052963
function base64Decode(base64) {
var binary_string = window.atob(base64);
return stringToArrayBuffer(binary_string);
}
// Converts a string to an ArrayBuffer.
function stringToArrayBuffer(value) {
var bytes = new Uint8Array(value.length);
for (var i = 0; i < value.length; i++) {
bytes[i] = value.charCodeAt(i);
}
return bytes.buffer;
}
decryptValue();
BTW, some of my code samples show the private key I'm using. That's intentional to help you understand the code (it's a throw away key). In fact, here's how I am getting the private key in C#:
private static RSACryptoServiceProvider GetPrivateKey()
{
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
RSAParameters RSAparams = new RSAParameters();
RSAparams.Modulus = Base64UrlDecode("oQeTwOlTc6rIb2kddwIOc0Ywslc7YzJSRZd_PegW7T3nO3DqCI5kp5EJmnGP8JJ9sbyVYyAHFLZQtMP69UspZFn__fBk2LTp2QdqBSMHbObENcSiG2FH-pZSwCaj3Pvy-qvTjnkxxN-3OE6oB8EcX5ekZwCZzAxazbVXctY_hCcaTWG7ugwc_ZyvhsdE7wa3pnTfXYHWXcDDT8FTpYl62aqWsEIUAJSkgmQ9zce0RiDUjBJyJEM9P0ihp1Ab8BD88pEM22-PXfiOesRzp5yOsjzI3kdr5KPsshstneJEGHYae5GZXLUpnVMRY1TCFFLbkPwK6oVkRaVU1RvK9ssO3Q");
RSAparams.Exponent = Base64UrlDecode("AQAB");
RSAparams.D = Base64UrlDecode("E4KDwgxy7jFrqeXqKjxPTGOdbEoZ2aWj5qcZhUJcnr9Qh_jg_grkgpHVwEbQifTxsipXTiR3_ygspI4XFoeV-wDVfWqWCVR3_bHChF9PW8Ak1x_dBSS28BMs8PdthI1pDbpqPhmMcF4riHCtNo1M1v8cLdeaiqiXitNVBkaTePsDiucfwOy1rgxwBqAL1CNJhP8oRiYkxD-gfE_EapWuXY9-wF9O-lXPLSTKWgMmmVxSmhUP-Uqk7cJ24UH9C7W7hnSQU4pkfD5XHx3_2WO2GMKKZcqz39wJUrQzrIO7539SYsQ3rEe4aMJyL4U-Ib4_purzVS0DRjzGxK8chT2guQ");
RSAparams.P = Base64UrlDecode("2TTEToB4AuPIPPpg3yTyBlGb_m-f4r-TxpU96ConV2p696_4QI6jlPWwgcC9Vdma_Da43AGuyLzIptgkzF8nSjV80VwwDKQ1YkFPc6ZqB2isvExuieSP6_jLlB-fCyCLqtTxpPm2VcK16Pqm0s5T0QGH6cQjjm1r2Ww1wuaiQbk");
RSAparams.Q = Base64UrlDecode("vcpFwkZKZ3hx3FpHgy3ScuuTRSPO2ge8TE8UMJdCrEnpftAeYuVYrJqnxfzKgyl02OijAUi1eozJxj_lM5McxrKZEEAvo6e8wtzl2hnkUh-KWoBJ8ii0VJcu6U5vs4pcv-lYBPFC6fzoGnUw8LNWMxb5ejgYbLUWp10BbfkWGEU");
RSAparams.DP = Base64UrlDecode("kibhWHk1R6yBlhZbjIrNl9beAkyV5vtFsj_F0ixbIITzjSqI_td71sWjKQvJ2rR7hu5DYTZ4p3XwBeQ2jpYQV-y5uh4v7rGngh-0GHuHqMiUQnejgYGcHgng4iCM4e3aTO7QUlP8jqRfxw6xpfNTjrVbAL8LtdCG21vmqOiLkXE");
RSAparams.DQ = Base64UrlDecode("qLF9x-zKfaXlLsNgBQ1ZnaQexrnJRqrRh9JSU85fCNy5mmpKWAUbCHB-59CGAId8wMAnAyEpjcBOKNTqWSlNzp84xeUHcyPI-Dt4Yp_Y_dXjGAYntALSJs4qeF2rk55MSpiSD_KSU4DknX_E_G2rFMY7AZOSwi1D8YcNmj5okTE");
RSAparams.InverseQ = Base64UrlDecode("Mza7JYleki7BvmD3dX5CO2nkD3mBGz4_0P_aoWyHEkWu4p5XWillaRVWyLnQEubLvAduUCr-lhfNmzdUhHecpE438_LQNtKRyOq9zkvjsMOGDmbkKpZ7-aTSshax6KNlYOWdOkadjuLtRExCmwbzu5lgI4NwacxSs5MfjHMrTCo");
RSA.ImportParameters(RSAparams);
return RSA;
}
// From the PDF here: https://www.rfc-editor.org/info/rfc7515
// Also see: https://auth0.com/docs/jwks
public static byte[] Base64UrlDecode(string arg)
{
string s = arg;
s = s.Replace('-', '+'); // 62nd char of encoding
s = s.Replace('_', '/'); // 63rd char of encoding
switch (s.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: s += "=="; break; // Two pad chars
case 3: s += "="; break; // One pad char
default:
throw new System.Exception(
"Illegal base64url string!");
}
return Convert.FromBase64String(s); // Standard base64 decoder
}
Because you're using OAEP with SHA-2-256 you need to move from RSACryptoServiceProvider to RSACng (.NET 4.6+). Note that aside from the ctor call, I've eliminated the knowledge of which implementation is being used.
private static RSA GetPrivateKey()
{
// build the RSAParams as before, then
RSA rsa = new RSACng();
rsa.ImportParameters(RSAparams);
return rsa;
}
// Setup encryption algorithm.
var provider = GetPrivateKey();
...
var decryptedData = provider.Decrypt(encryptedData, RSAEncryptionPadding.OaepSHA256);
I am unable to test #bartonjs's answer because I don't have access to a Windows computer and Mono apparently doesn't implement RSACng. Below is an example that decrypts your ciphertext using the Bouncycastle C# library. Notice the OaepPadding(...) uses SHA-256 for both the Oaep hash and the MGF hash. This is apparently what is needed to interoperate with your javascript code. Also, notice I used Encoding.UTF8.GetString() whereas you used Encoding.Unicode.GetString(). The encoding is definitely not UTF-16 which is what Encoding.Unicode gives you.
using System;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Encodings;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
namespace RsaSha256OaepDecrypt
{
class MainClass
{
public static void Main(string[] args)
{
var encryptedValue = "CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==";
var encryptedData = Convert.FromBase64String(encryptedValue);
var rsaPrivate = GetPrivateKey();
IAsymmetricBlockCipher cipher0 = new RsaBlindedEngine();
cipher0 = new OaepEncoding(cipher0, new Sha256Digest(), new Sha256Digest(), null);
BufferedAsymmetricBlockCipher cipher = new BufferedAsymmetricBlockCipher(cipher0);
cipher.Init(false, rsaPrivate);
cipher.ProcessBytes(encryptedData, 0, encryptedData.Length);
var decryptedData = cipher.DoFinal();
var decryptedText = Encoding.UTF8.GetString(decryptedData);
Console.WriteLine(decryptedText);
}
private static BigInteger makeBigInt(String b64Url)
{
var bytes = Base64UrlDecode(b64Url);
if ((sbyte)bytes[0] < 0)
{
// prepend a zero byte to make it positive.
var bytes1 = new byte[bytes.Length + 1];
bytes1[0] = 0;
bytes.CopyTo(bytes1, 1);
bytes = bytes1;
}
return new BigInteger(bytes);
}
private static AsymmetricKeyParameter GetPrivateKey()
{
//RSAParameters RSAparams = new RSAParameters();
var Modulus = makeBigInt("oQeTwOlTc6rIb2kddwIOc0Ywslc7YzJSRZd_PegW7T3nO3DqCI5kp5EJmnGP8JJ9sbyVYyAHFLZQtMP69UspZFn__fBk2LTp2QdqBSMHbObENcSiG2FH-pZSwCaj3Pvy-qvTjnkxxN-3OE6oB8EcX5ekZwCZzAxazbVXctY_hCcaTWG7ugwc_ZyvhsdE7wa3pnTfXYHWXcDDT8FTpYl62aqWsEIUAJSkgmQ9zce0RiDUjBJyJEM9P0ihp1Ab8BD88pEM22-PXfiOesRzp5yOsjzI3kdr5KPsshstneJEGHYae5GZXLUpnVMRY1TCFFLbkPwK6oVkRaVU1RvK9ssO3Q");
var Exponent = makeBigInt("AQAB");
var D = makeBigInt("E4KDwgxy7jFrqeXqKjxPTGOdbEoZ2aWj5qcZhUJcnr9Qh_jg_grkgpHVwEbQifTxsipXTiR3_ygspI4XFoeV-wDVfWqWCVR3_bHChF9PW8Ak1x_dBSS28BMs8PdthI1pDbpqPhmMcF4riHCtNo1M1v8cLdeaiqiXitNVBkaTePsDiucfwOy1rgxwBqAL1CNJhP8oRiYkxD-gfE_EapWuXY9-wF9O-lXPLSTKWgMmmVxSmhUP-Uqk7cJ24UH9C7W7hnSQU4pkfD5XHx3_2WO2GMKKZcqz39wJUrQzrIO7539SYsQ3rEe4aMJyL4U-Ib4_purzVS0DRjzGxK8chT2guQ");
var P = makeBigInt("2TTEToB4AuPIPPpg3yTyBlGb_m-f4r-TxpU96ConV2p696_4QI6jlPWwgcC9Vdma_Da43AGuyLzIptgkzF8nSjV80VwwDKQ1YkFPc6ZqB2isvExuieSP6_jLlB-fCyCLqtTxpPm2VcK16Pqm0s5T0QGH6cQjjm1r2Ww1wuaiQbk");
var Q = makeBigInt("vcpFwkZKZ3hx3FpHgy3ScuuTRSPO2ge8TE8UMJdCrEnpftAeYuVYrJqnxfzKgyl02OijAUi1eozJxj_lM5McxrKZEEAvo6e8wtzl2hnkUh-KWoBJ8ii0VJcu6U5vs4pcv-lYBPFC6fzoGnUw8LNWMxb5ejgYbLUWp10BbfkWGEU");
var DP = makeBigInt("kibhWHk1R6yBlhZbjIrNl9beAkyV5vtFsj_F0ixbIITzjSqI_td71sWjKQvJ2rR7hu5DYTZ4p3XwBeQ2jpYQV-y5uh4v7rGngh-0GHuHqMiUQnejgYGcHgng4iCM4e3aTO7QUlP8jqRfxw6xpfNTjrVbAL8LtdCG21vmqOiLkXE");
var DQ = makeBigInt("qLF9x-zKfaXlLsNgBQ1ZnaQexrnJRqrRh9JSU85fCNy5mmpKWAUbCHB-59CGAId8wMAnAyEpjcBOKNTqWSlNzp84xeUHcyPI-Dt4Yp_Y_dXjGAYntALSJs4qeF2rk55MSpiSD_KSU4DknX_E_G2rFMY7AZOSwi1D8YcNmj5okTE");
var InverseQ = makeBigInt("Mza7JYleki7BvmD3dX5CO2nkD3mBGz4_0P_aoWyHEkWu4p5XWillaRVWyLnQEubLvAduUCr-lhfNmzdUhHecpE438_LQNtKRyOq9zkvjsMOGDmbkKpZ7-aTSshax6KNlYOWdOkadjuLtRExCmwbzu5lgI4NwacxSs5MfjHMrTCo");
var rsa = new RsaPrivateCrtKeyParameters(Modulus, Exponent, D, P, Q, DP, DQ, InverseQ);
return rsa;
}
// From the PDF here: https://www.rfc-editor.org/info/rfc7515
// Also see: https://auth0.com/docs/jwks
public static byte[] Base64UrlDecode(string arg)
{
string s = arg;
s = s.Replace('-', '+'); // 62nd char of encoding
s = s.Replace('_', '/'); // 63rd char of encoding
switch (s.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: s += "=="; break; // Two pad chars
case 3: s += "="; break; // One pad char
default:
throw new System.Exception(
"Illegal base64url string!");
}
return Convert.FromBase64String(s); // Standard base64 decoder
}
}
}
I'm trying to
generate sign/verification keys (RSA)
sign a value (using those keys) on a Java web application (lets call server-side)
in order to a web client to verify - public-key imported as RSASSA-PKCS1-v1_5 + SHA-256, (in a browser, using WebCrypto API / client-side)
I'm having problems verifying the signed value (signed in the Java server-side) even though the public sign/verify key is successfully imported as a JWK in the client side.
I was wondering if there is any algorithm compatibility issue in any of the steps (OpenSSL, Java or Javascript) that I may be encountering.
The OpenSSL commands used to generate the keys
openssl genrsa -out privatekey.pem 2048
openssl rsa -in privatekey.pem -pubout > publickey.pub
openssl pkcs8 -topk8 -inform PEM -outform DER -in privatekey.pem -out privatekey-pkcs8.pem
Importing keys with Java (server-side)
public static KeyPair generateSignKeyPair() throws ... {
byte[] privBytes = b64ToByteArray(PRIVATE_KEY_PEM_VALUE);
byte[] pubBytes = b64ToByteArray(PUBLIC_KEY_PEM_VALUE);
// private key
KeySpec keySpec = new PKCS8EncodedKeySpec(privBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// public key (javaPubSignKey)
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(pubBytes);
PublicKey publicKey = keyFactory.generatePublic(X509publicKey);
return new KeyPair(publicKey, privateKey);
}
Signing a value with Java (server-side)
public static byte[] generateSignature(PrivateKey signPrivateKey, byte[] data) throws ... {
Signature dsa = Signature.getInstance("SHA256withRSA");
dsa.initSign(signPrivateKey);
dsa.update(data);
return dsa.sign();
}
Send them to a web-app for the WebCrypto API to verify as a client/browser (the client is aware of the publicKey generated in the first step).
// Import public sign/verify key (javaPubSignVerifyKey)
var signatureAlgorithm = {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {
name: 'SHA-256'
}
};
// JWK format (1)
crypto.subtle.importKey(
'jwk', javaPubSignVerifyKey, signatureAlgorithm, false, ['verify']
).then(success, error);
function success(key) {
signatureVerifyPublicKey = key;
}
Note (1): on the Java side, I'm using com.nimbusds.jose.jwk.JWK to export the publicKey to JWK format.
The sign key is successfully imported by WebCrypto. But when it comes to the verification, it fails (the verification boolean is false).
crypto.subtle.verify(
signatureAlgorithm,
signatureVerifyPublicKey,
signature, // bytes in Int8Array format (2)
data // bytes in Int8Array format
).then(
function (valid) {
// valid === false
}
)
Note (2): also note that every example I found on WebCrypto used Uint8Array to represent byte arrays, but since Java generates signed byte-arrays I need to use Int8Array so that the signature values are not contaminated (maybe this is an issue aswell).
EDIT: for reference, it turned out to be another unrelated issue - I was converting the expected data from base64 twice in Javascript without noticing it; naturally the verification failed.
Please, check this simple code based on yours to import a RSA public key (spki) and verify a signature. I have generated the keys and signature using similar Java code
var publicKeyB64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVdZDEs6htb3oxWstz7q+e5IwIRcptMNJiyemuoNyyjtiOy+0tEodjgo7RVoyUcGU3MysEivqvKdswQZ4KfwQCBLAR8DRzp3biAge5utZcKsQoQaC1rCEplfmzEo5ovIlBcMq5x1BxnrnlwEPRmM7MefRa+OeAOQJcstHcrJFO7QIDAQAB";
var dataB64 = "aGVsbG8=";
var signatureB64 = "aEOmUA7YC5gvF6QgH+TMg0erY5pzr83nykZGFtyGOOe+6ld+MC4/Qdb608XiNud+pBpzh0wqd6aajOtJim5XEfCH8vUPsv45aSPtukUIQTX00Oc1frIFDQI6jGJ4Q8dQYIwpqsyE2rkGwTDzt1fTTGiw54pLsJXjtL/D5hUEKL8=";
var signatureAlgorithm = {name: 'RSASSA-PKCS1-v1_5',modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]),hash: { name: 'SHA-256' }};
//convert public key, data and signature to ArrayBuffer.
var publicKey = str2ab(atob(publicKeyB64));
var data = str2ab(atob(dataB64));
var signature = str2ab(atob(signatureB64));
crypto.subtle.importKey("spki", publicKey, signatureAlgorithm, false,["verify"]).
then(function(key){
console.log(key);
return crypto.subtle.verify( signatureAlgorithm, key, signature, data);
}).then( function (valid) {
console.log("Signature valid: "+valid);
}).catch(function(err) {
alert("Verification failed " + err );
});
I could not reproduce exactly the issue. Using the str2ab utility function you have linked, the code works perfectly.
//Utility function
function str2ab(str) {
var arrBuff = new ArrayBuffer(str.length);
var bytes = new Uint8Array(arrBuff);
for (var iii = 0; iii < str.length; iii++) {
bytes[iii] = str.charCodeAt(iii);
}
return bytes;
}
I suggest to compare both codes to find the differences