I have a client in JavaScript and a server in Node.JS. I'm trying to sign a simple text in client and send the signature along with publicKey to the server then server can verify the publicKey.
Anything in client-side is OK! but I'm unable to verify the signature in server-side. I think there is no need for you to read the client code but just for assurance I'll provide it too.
Client code:
let privateKey = 0;
let publicKey = 0;
let encoded = '';
let signatureAsBase64 = '';
let pemExported = ''
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
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;
}
let keygen = crypto.subtle.generateKey({
name: 'RSA-PSS',
modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]),
hash: 'SHA-256'
}, true, ['sign', 'verify']);
keygen.then((value)=>{
publicKey = value.publicKey;
privateKey = value.privateKey;
let exported = crypto.subtle.exportKey('spki', publicKey);
return exported
}).then((value)=>{
console.log('successful');
const exportedAsString = ab2str(value);
const exportedAsBase64 = btoa(exportedAsString);
pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;
//signing:
encoded = new TextEncoder().encode('test');
let signing = crypto.subtle.sign({
name: "RSA-PSS",
saltLength: 32
},
privateKey,
encoded);
return signing;
}).then((signature)=>{
const signatureAsString = ab2str(signature);
signatureAsBase64 = btoa(signatureAsString);
//verifying just to be sure everything is OK:
return crypto.subtle.verify({
name: 'RSA-PSS',
saltLength: 32
},
publicKey,
signature,
encoded)
}).then((result)=>{
console.log(result);
//send information to server:
let toSend = new XMLHttpRequest();
toSend.onreadystatechange = ()=>{
console.log(this.status);
};
toSend.open("POST", "http://127.0.0.1:3000/authentication", true);
let data = {
signature: signatureAsBase64,
publicKey: pemExported
};
toSend.setRequestHeader('Content-Type', 'application/json');
toSend.send(JSON.stringify(data));
//to let you see the values, I'll print them to console in result:
console.log("signature is:\n", signatureAsBase64);
console.log("publicKey is:\n", pemExported);
}).catch((error)=>{
console.log("error",error.message);
})
Server Code(I use express for this purpose):
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
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;
}
router.post('/authentication', async (req, res)=>{
try{
const publicKey = crypto.createPublicKey({
key: req.body.publicKey,
format: 'pem',
type: 'spki'
});
console.log(publicKey.asymmetricKeyType, publicKey.asymmetricKeySize, publicKey.type);
let signature = Buffer.from(req.body.signature, 'base64').toString();
signature = str2ab(signature);
const result = crypto.verify('rsa-sha256', new TextEncoder().encode('test'),
publicKey, new Uint8Array(signature));
console.log(result);
}catch(error){
console.log('Error when autheticating user: ', error.message);
}
})
Server Console Log:
rsa undefined public
false
NOTE:
I think the public key is imported correctly in server because when I export the
public key again in server, the pem formats of both sides(client & server) are completely
equal. so I think the problem is associated with 'verification' or 'converting signature' in server.
I prefer to use the built-in crypto module if it's possible, so other libraries such as subtle-crypto are my second options and I'm here to see if this can be done with crypto or not.
I want to learn how to verify a signature that is signed by JavaScript SubtleCrypto, due to this, Please don't ask some questions such as:
Why do you want to verify the public key in server?
Why don't you use 'X' library in client?
Feel free to change Exported format(pem), Public key format('spki'), Algorithm format(RSA-PSS) and so on.
The failed verification has two reasons:
The PSS padding must be specified explicitly, since PKCS#1 v1.5 padding is the default, s. here.
The conversion of the signature corrupts the data: The line:
let signature = Buffer.from(req.body.signature, 'base64').toString();
performs a UTF8 decoding, s. here, which irreversibly changes the data, s. here. The signature consists of binary data that is generally UTF8 incompatible. A conversion to a string is only possible with suitable binary-to-text encodings (like Base64, hex etc.), s. here.But apart from that a conversion is actually not necessary at all, because the signature can be passed directly as a buffer, s. here.
The following NodeJS code performs a successful verification (for a signature and public key produced with the client code):
const publicKey = crypto.createPublicKey(
{
key: req.body.publicKey,
format: 'pem',
type: 'spki'
});
const result = crypto.verify(
'rsa-sha256',
new TextEncoder().encode('test'),
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
},
Buffer.from(req.body.signature, 'base64'));
console.log(result); // true
Related
I'm trying to exchange public keys between Browser and Server, and generate secret to be used for encryption of data. I'm trying to utilize ECDH (Elliptic Curve Diffie-Hellman).
On the Server side I'm generating ECDH with prime256v1 algorithm.
On the Browser side I'm generating ECDH with P-256 named curve. (these algorithms should be the same, they are just named differently, P-256 , also known as secp256r1 and prime256v1).
I'm able to pass Browser generated public key to the server as Base64 formatted string, and to generate secret using Server private key and Browser public key. And everything works fine on the Server side (import, generate secret, encryption).
But when I try to pass Server generated public key to the Browser as Base64 formatted string and try to import it, I get DOMException: Cannot create a key using the specified key usages.
const b64ToBin = (b64) => {
const binaryString = window.atob(b64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
const importB64Key = async (base64key) => {
const bin = b64ToBin(base64key);
console.log('bin ', bin);
const key = await window.crypto.subtle.importKey(
'raw',
bin,
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
['deriveKey']
);
return key;
};
The keyUsages value is wrong (as also pointed out by the error message): When importing a public key in the context of ECDH, an empty array [] is to be passed for keyUsages (deriveKey and/or deriveBits are only passed when importing a private key).
Also, keep in mind that the public EC key must be passed as an uncompressed or compressed key when using the format raw.
If the keyUsages value is fixed and an uncompressed or compressed key is applied, the posted code works.
(async () => {
var uncomressedKeyB64 = 'BAmL07vrRR5lfkWuH1RAFJufx0B4J+BdOqIYZCH+fJc8c+5sFch8aXEJ6qVgTnnYjKwrQ1BO3Tg28/F1h/FjMVQ=';
var comressedKeyB64 = 'AgmL07vrRR5lfkWuH1RAFJufx0B4J+BdOqIYZCH+fJc8';
const b64ToBin = (b64) => {
const binaryString = window.atob(b64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
const importB64Key = async (base64key) => {
const bin = b64ToBin(base64key);
console.log('bin ', bin);
const key = await window.crypto.subtle.importKey(
'raw',
bin,
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
[] // Fix: When importing a public key, an empty array must be passed for the key usages...
);
return key;
};
console.log(await importB64Key(uncomressedKeyB64));
console.log(await importB64Key(comressedKeyB64));
})();
I am attempting to fetch encrypted raw buffer data (AES-256) from Arweave, pass to a decrypt function and use this to display an image. I am trying to fetch and decrypt the ArrayBuffer on the front end (in my React app).
First, I am encrypting the Buffer data in NodeJS and storing the file. Here is the code for it:
/**********************
** Runs in NodeJS **
**********************/
const encrypt = (dataBuffer, key) => {
// Create an initialization vector
const iv = crypto.randomBytes(IV_LENGTH);
// Create cipherKey
const cipherKey = Buffer.from(key);
// Create cipher
const cipher = crypto.createCipheriv(ALGORITHM, cipherKey, iv);
const encryptedBuffer = Buffer.concat([
cipher.update(dataBuffer),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
let bufferLength = Buffer.alloc(1);
bufferLength.writeUInt8(iv.length, 0);
return Buffer.concat([bufferLength, iv, authTag, encryptedBuffer]);
};
const encryptedData = encrypt(data, key)
fs.writeFile("encrypted_data.enc", encryptedData, (err) => {
if(err){
return console.log(err)
}
});
Next, I try to fetch and decrypt on the front-end. What I have so far returns an ArrayBuffer from the response. I try to pass this ArrayBuffer to the decrypt function. Here is the code:
/***********************
** Runs in React **
***********************/
import crypto from "crypto"
const getData = async (key) => {
const result = await (await fetch('https://arweave.net/u_RwmA8gP0DIEeTBo3pOQTJ20LH2UEtT6LWjpLidOx0/encrypted_data.enc')).arrayBuffer()
const decryptedBuffer = decrypt(result, key)
console.log(decryptedBuffer)
}
// Here is the decrypt function I am passing the ArrayBuffer to:
export const decrypt = (dataBuffer, key) => {
// Create cipherKey
const cipherKey = Buffer.from(key);
// Get iv and its size
const ivSize = dataBuffer.readUInt8(0);
const iv = dataBuffer.slice(1, ivSize + 1);
// Get authTag - is default 16 bytes in AES-GCM
const authTag = dataBuffer.slice(ivSize + 1, ivSize + 17);
// Create decipher
const decipher = crypto.createDecipheriv("aes-256-gcm", cipherKey, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([
decipher.update(dataBuffer.slice(ivSize + 17)),
decipher.final(),
]);
};
When I pass the ArrayBuffer data to the decrypt function, I get this error:
Unhandled Rejection (TypeError): First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
You're omitting a lot of details that would help the community understand how you're encrypting the image, how you're retrieving it, and how you're decrypting it. Here's a full example of fetching an image, encrypting it, decrypting it, and displaying it in the browser. This runs in Chrome v96 and Firefox v95.
(async () => {
const encryptionAlgoName = 'AES-GCM'
const encryptionAlgo = {
name: encryptionAlgoName,
iv: window.crypto.getRandomValues(new Uint8Array(12)) // 96-bit
}
// create a 256-bit AES encryption key
const encryptionKey = await crypto.subtle.importKey(
'raw',
new Uint32Array([1,2,3,4,5,6,7,8]),
{ name: encryptionAlgoName },
true,
["encrypt", "decrypt"],
)
// fetch a JPEG image
const imgBufferOrig = await (await fetch('https://fetch-progress.anthum.com/images/sunrise-baseline.jpg')).arrayBuffer()
// encrypt the image
const imgBufferEncrypted = await crypto.subtle.encrypt(
encryptionAlgo,
encryptionKey,
imgBufferOrig
)
// decrypt recently-encrypted image
const imgBufferDecrypted = await crypto.subtle.decrypt(
encryptionAlgo,
encryptionKey,
imgBufferEncrypted
)
// display unencrypted image
const img = document.createElement('img')
img.style.maxWidth = '100%'
img.src = URL.createObjectURL(
new Blob([ imgBufferDecrypted ])
)
document.body.append(img)
})()
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));
}
On iOS app I successfully encrypt/decrypt a string using AES128 algorithm and PKCS7 padding. I have used several AES online tools to encrypt/decrypt but the results are not the same. For example: http://www.txtwizard.net/crypto
For testing purposes I am using:
string to encrypt: You will find the ruby at position (x,7)
key: sec_key_16_bytes
iv: ivr_key_16_bytes
On iOS I obtain the string encrypted (base64): AAAAABAAAABwhAlwAQAAQGSHCxTygRNNvTrRNPtV6SV4eRSAkMyyToXq9XN6cEpip8QDuoV9Bkv0phJS4pocLQ==
I would like to obtain same result on a Node.js instance or simple JavaScript on client-side.
I've looked into crypto official documentation, but it has something like
const crypto = require('crypto');
const algorithm = 'aes-192-cbc';
const password = 'Password used to generate key';
// Key length is dependent on the algorithm. In this case for aes192
// it is 24 bytes (192 bits).
// Use the async `crypto.scrypt()` instead.
const key = crypto.scryptSync(password, 'salt', 24);
// The IV is usually passed along with the ciphertext.
const iv = Buffer.alloc(16, 0); // Initialization vector.
Here I have also salt and password, while on iOS I have only the algorithm and key.
This is the Swift code:
import Foundation
import CommonCrypto
// Advanced Encryption Standard (symmetric key algorithm)
// More at this great tutorial:
http://www.splinter.com.au/2019/06/09/pure-swift-common-crypto-aes-
encryption/
protocol Cryptable {
func encrypt(_ string: String) throws -> Data
func decrypt(_ data: Data) throws -> String
}
struct AES {
private let key: Data
private let ivSize: Int = kCCBlockSizeAES128
private let options: CCOptions = CCOptions(kCCOptionPKCS7Padding)
init(keyString: String) throws {
guard keyString.count == kCCKeySizeAES128 else {
throw Error.invalidKeySize
}
self.key = Data(keyString.utf8)
}
static func test() {
do {
let aes = try AES(keyString: "sec_key_16_bytes")
let stringToEncrypt: String = "You will find the ruby at position (x,7)"
print("String to encrypt:\t\t\t\(stringToEncrypt)")
let encryptedData: Data = try aes.encrypt(stringToEncrypt)
print("String encrypted (base64):\t\(encryptedData.base64EncodedString())")
let decryptedData: String = try aes.decrypt(encryptedData)
print("String decrypted:\t\t\t\(decryptedData)")
} catch {
print("Something went wrong: \(error)")
}
}
}
extension AES {
enum Error: Swift.Error {
case invalidKeySize
case generateRandomIVFailed
case encryptionFailed
case decryptionFailed
case dataToStringFailed
}
}
private extension AES {
func generateRandomIV(for data: inout Data) throws {
let string = "ivr_key_16_bytes"
let value = string.data(using: .utf8) ?? Data()
try data.withUnsafeMutableBytes { dataBytes in
guard let dataBytesBaseAddress = dataBytes.baseAddress else {
throw Error.generateRandomIVFailed
}
// let status: Int32 = SecRandomCopyBytes(
// kSecRandomDefault,
// kCCBlockSizeAES128,
// dataBytesBaseAddress
// )
dataBytesBaseAddress.storeBytes(of: value, as: Data.self)
// guard status == 0 else {
// throw Error.generateRandomIVFailed
// }
}
}
}
extension AES: Cryptable {
func encrypt(_ string: String) throws -> Data {
let dataToEncrypt = Data(string.utf8)
let bufferSize: Int = ivSize + dataToEncrypt.count + kCCBlockSizeAES128
var buffer = Data(count: bufferSize)
try generateRandomIV(for: &buffer)
var numberBytesEncrypted: Int = 0
do {
try key.withUnsafeBytes { keyBytes in
try dataToEncrypt.withUnsafeBytes { dataToEncryptBytes in
try buffer.withUnsafeMutableBytes { bufferBytes in
guard let keyBytesBaseAddress = keyBytes.baseAddress,
let dataToEncryptBytesBaseAddress = dataToEncryptBytes.baseAddress,
let bufferBytesBaseAddress = bufferBytes.baseAddress else {
throw Error.encryptionFailed
}
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
CCOperation(kCCEncrypt), // op: CCOperation
CCAlgorithm(kCCAlgorithmAES), // alg: CCAlgorithm
options, // options: CCOptions
keyBytesBaseAddress, // key: the "password"
key.count, // keyLength: the "password" size
bufferBytesBaseAddress, // iv: Initialization Vector
dataToEncryptBytesBaseAddress, // dataIn: Data to encrypt bytes
dataToEncryptBytes.count, // dataInLength: Data to encrypt size
bufferBytesBaseAddress + ivSize, // dataOut: encrypted Data buffer
bufferSize, // dataOutAvailable: encrypted Data buffer size
&numberBytesEncrypted // dataOutMoved: the number of bytes written
)
guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
throw Error.encryptionFailed
}
}
}
}
} catch {
throw Error.encryptionFailed
}
let encryptedData: Data = buffer[..<(numberBytesEncrypted + ivSize)]
return encryptedData
}
func decrypt(_ data: Data) throws -> String {
let bufferSize: Int = data.count - ivSize
var buffer = Data(count: bufferSize)
var numberBytesDecrypted: Int = 0
do {
try key.withUnsafeBytes { keyBytes in
try data.withUnsafeBytes { dataToDecryptBytes in
try buffer.withUnsafeMutableBytes { bufferBytes in
guard let keyBytesBaseAddress = keyBytes.baseAddress,
let dataToDecryptBytesBaseAddress = dataToDecryptBytes.baseAddress,
let bufferBytesBaseAddress = bufferBytes.baseAddress else {
throw Error.encryptionFailed
}
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
CCOperation(kCCDecrypt), // op: CCOperation
CCAlgorithm(kCCAlgorithmAES128), // alg: CCAlgorithm
options, // options: CCOptions
keyBytesBaseAddress, // key: the "password"
key.count, // keyLength: the "password" size
dataToDecryptBytesBaseAddress, // iv: Initialization Vector
dataToDecryptBytesBaseAddress + ivSize, // dataIn: Data to decrypt bytes
bufferSize, // dataInLength: Data to decrypt size
bufferBytesBaseAddress, // dataOut: decrypted Data buffer
bufferSize, // dataOutAvailable: decrypted Data buffer size
&numberBytesDecrypted // dataOutMoved: the number of bytes written
)
guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
throw Error.decryptionFailed
}
}
}
}
} catch {
throw Error.encryptionFailed
}
let decryptedData: Data = buffer[..<numberBytesDecrypted]
guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
throw Error.dataToStringFailed
}
return decryptedString
}
}
The result in the iOS console is:
String to encrypt: You will find the ruby at position (x,7)
String encrypted (base64): AAAAABAAAABwhAlwAQAAQGSHCxTygRNNvTrRNPtV6SV4eRSAkMyyToXq9XN6cEpip8QDuoV9Bkv0phJS4pocLQ==
String decrypted: You will find the ruby at position (x,7)
How can I achieve same result with JavaScript or server-side? I am setting incorrect options in Swift CCCrypt? Are there any cross-platform solutions?
Thank you!
In my logic i am hashing some data based on a secret key. Later I want to verify that signature. I am using the crypto package in nodejs. Specifically, in the verifier.verify function, the docs require a publicKey. How would i go upon doing this as i am using the secret in config?
Any help would be awesome!
let data = {
"data": 15
}
config: {
secret: 'mgfwoieWCVBVEW42562tGVWS',
}
let stringData = JSON.stringify(data)
const hash = crypto.createHmac('sha256', config.secret)
.update(stringData, 'utf-8')
.digest('base64')
const verifier = crypto.createVerify('sha256')
let ver = verifier.verify(publicKey, stringData, 'base64')
console.log(ver);
If you want to verify a particular signature in node you can use the following
config: {
secret: "IloveMyself"
};
var cipherPassword = crypto.createCipher('aes-256-ctr', config.secret);
var dbpassword = cipherPassword.update(dbpassword, 'utf-8', 'hex');
This will be for creating encryption of the password. Now to verify the password/signature again in NodeJS, you need to do the following:
var cipher = crypto.createDecipher('aes-256-ctr', config.secret);
var dbPassword = cipher.update(dbpassword, 'hex', 'utf-8');
This will give decrypted password/signature which you can compare easily. I hope it helps!