Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@

const platformClient = require('purecloud-platform-client-v2');
const winston = require('winston');
const fs = require('fs');
const path = require('path')
const {spawn} = require('child_process');

const clientId = process.env.GENESYS_CLOUD_CLIENT_ID;
const clientSecret = process.env.GENESYS_CLOUD_CLIENT_SECRET;

// Set the Genesys Cloud region
const client = platformClient.ApiClient.instance;
client.setEnvironment(platformClient.PureCloudRegionHosts.us_east_1);

// Create API instance
const recordingApi = new platformClient.RecordingApi();

// Use winston for logging
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(), winston.format.prettyPrint()
),
transports: [
new winston.transports.File({
filename: `./byoiclient-encrypt-recording.log`,
}),
// Output to console for error logs
new winston.transports.Console({
level: 'error',
})
]
});


/* Demonstrate the process to encrypt a recording:
*
* Login OAuth client with client ID and secret
* Fetch the most recent recording key
* Generate X509 certificate
* Encrypt the recording file using the certificate
* Catch and log any error
*/
// >> START byoi-generate-intermediate-key-cipherv2
// Use OpenSSL 3.0.x to convert DER to RSA Public Key
async function convertDerToRSAPublicKey(opensslPath, derFileName, pemFileName) {
return new Promise((resolve, reject) => {
// openssl rsa -RSAPublicKey_in -in derFileName.der -inform DER -outform PEM -out pemFileName.pem
let parameters = [
'rsa', '-RSAPublicKey_in',
'-in', derFileName,
'-inform', 'DER', '-outform', 'PEM',
'-out', pemFileName
];

let opensslProcess = spawn(opensslPath, parameters);
opensslProcess.on('close', (code) => {
if (code == 0) {
resolve(true);
} else {
logger.error(`OpenSSL returned ${code} when converting public key`);
opensslProcess.stdout.pipe(process.stdout);
resolve(null);
}
});
});
}

// Run an OpenSSL command and return a promise that resolves to true on success
function runOpenssl(opensslPath, parameters) {
return new Promise((resolve) => {
let opensslProcess = spawn(opensslPath, parameters);
opensslProcess.on('close', (code) => {
if (code == 0) {
resolve(true);
} else {
logger.error(`OpenSSL returned ${code} for command: openssl ${parameters[0]}`);
resolve(false);
}
});
});
}

// Generate an X509 certificate embedding the Genesys public key, using OpenSSL (FIPS-validated module) with SHA-256 signing
async function generateX509Certificate(opensslPath, keyId, pemFileName, certFileName) {
let throwawayKeyFile = certFileName.replace('.cert.pem', '.throwaway.key');
let csrFile = certFileName.replace('.cert.pem', '.csr');

// Serial number must start with "00" and not contain dashes
let serialHex = '0x00' + keyId.replace(/-/g, '');

// Step 1: Generate a throwaway 2048-bit RSA private key for certificate signing
let success = await runOpenssl(opensslPath, [
'genpkey', '-algorithm', 'RSA',
'-pkeyopt', 'rsa_keygen_bits:2048',
'-out', throwawayKeyFile
]);
if (!success) return false;

// Step 2: Create a certificate signing request using the throwaway key
success = await runOpenssl(opensslPath, [
'req', '-new',
'-key', throwawayKeyFile,
'-subj', '/CN=recording-encryption',
'-out', csrFile
]);
if (!success) return false;

// Step 3: Create self-signed cert with the Genesys public key forced in, signed with SHA-256
success = await runOpenssl(opensslPath, [
'x509', '-req',
'-in', csrFile,
'-force_pubkey', pemFileName,
'-signkey', throwawayKeyFile,
'-set_serial', serialHex,
'-sha256',
'-days', '1',
'-out', certFileName
]);

// Clean up throwaway intermediate files
try { fs.unlinkSync(throwawayKeyFile); } catch(e) { /* ignore */ }
try { fs.unlinkSync(csrFile); } catch(e) { /* ignore */ }

return success;
}
// >> END byoi-generate-intermediate-key-cipherv2
// >> START byoi-perform-encryption-cipherv2
// Invoke openssl command to encrypt the file using the certificate file (FIPS 140-3 compliant: AES-256-GCM + RSA-OAEP)
async function performEncryption(opensslPath, x509CertificateFilePath, originalFilePath, outputFilePath) {
return new Promise((resolve, reject) => {
// openssl cms -encrypt -aes-256-gcm -recip f36f8c85-8922-45fa-a3f1-d197d1176340.cert.pem -keyopt rsa_padding_mode:oaep -keyopt rsa_oaep_md:sha256 -in audio.opus -binary -outform DER -out audio.opus.bin
// The recipient certificate must be supplied with -recip and must appear BEFORE -keyopt.
// -keyopt applies to the preceding recipient, so passing the certificate as a trailing
// argument instead would make OpenSSL fail with "No key specified".
// rsa_oaep_md:sha256 is required as well as rsa_padding_mode:oaep. Without it OpenSSL omits the
// RSAESOAEPparams from the CMS envelope, which means SHA-1 by default (RFC 8017), and Genesys Cloud
// will not be able to decrypt the recording. Both flags require OpenSSL 3.0 or later.
let parameters = [
'cms', '-encrypt',
'-aes-256-gcm',
'-recip', x509CertificateFilePath,
'-keyopt', 'rsa_padding_mode:oaep',
'-keyopt', 'rsa_oaep_md:sha256',
'-in', originalFilePath,
'-binary', '-outform', 'DER',
'-out', outputFilePath
];

let opensslProcess = spawn(opensslPath, parameters);
opensslProcess.on('close', (code) => {
if (code == 0) {
resolve(true);
} else {
logger.error(`OpenSSL returned ${code} when performing encryption`);
opensslProcess.stdout.pipe(process.stdout);
resolve(false);
}
});
});
}
// >> END byoi-perform-encryption-cipherv2
// Encrypt recording file (e.g. recordingFile.opus => recordingFile.opus.bin)
async function encryptRecording(recordingAudioFile) {
try {

// Specify the path to OpenSSL executable. Please specify the absolute path if there are multiple OpenSSL versions.
// This example uses OpenSSL 3.0.7 to perform CMS encryption. Other equivalent CMS encryption tools should also work
// but these have not been tested. Run "openssl version" to get the version
const opensslPath = 'openssl';

// Create folder for saving temp files
let workingDir = path.join(__dirname, 'temp');

// Create the folders if they do not exist
if(!fs.existsSync(workingDir)) {
fs.mkdirSync(workingDir);
}
// >> START byoi-get-encryption-key-cipherv2
// Login OAuth client
let response = await client.loginClientCredentialsGrant(clientId, clientSecret);
logger.verbose('Login successfully');

// Fetch the most recent recording key by specifying page 1 with a page size of 1
// Ensure that only a single key is returned
response = await recordingApi.getRecordingRecordingkeys({
'pageSize': 1,
'pageNumber': 1
});
logger.verbose(response);
if(response.total !== 1) {
logger.error('Failed to retrieve public recording key');
return false;
}

let keyId = response.entities[0].id;
let publicKey = response.entities[0].keydataSummary;
// >> END byoi-get-encryption-key-cipherv2
// >> START byoi-encrypt-file-cipherv2
// OpenSSL CMS encryption requires X509 certificate, so the public key needs to be converted to X509 certificate
// Save the public key to the binary DER as .der file
let derFileName = path.join(workingDir, keyId + '.der');
let keyDer = Buffer.from(publicKey, 'base64');
var stream = fs.createWriteStream(derFileName);
stream.write(keyDer);

// Convert DER to RSA Public Key and save it as .pem file
let pemFileName = path.join(workingDir, keyId + '.pem');
await convertDerToRSAPublicKey(opensslPath, derFileName, pemFileName);

// Generate X509 certificate and save the certificate as .cert.pem file
let certFileName = path.join(workingDir, keyId + '.cert.pem');
await generateX509Certificate(opensslPath, keyId, pemFileName, certFileName);
logger.verbose(`Generated X509 certificate file: ${certFileName}`);

// Perform CMS encryption using the certificate file
let fileToEncrypt = path.join(__dirname, recordingAudioFile);
let outputFile = fileToEncrypt + '.bin';
let isFileEncrypted = await performEncryption(opensslPath, certFileName, fileToEncrypt, outputFile);
// >> END byoi-encrypt-file-cipherv2
if(isFileEncrypted) {
logger.info(`File ${fileToEncrypt} has been encrypted into ${outputFile}`);
}
else {
logger.error(`Failed to encrypt file ${fileToEncrypt}`);
}
}
catch(err) {
// Directly logging an error in winston would result in empty string
if (err instanceof Error) {
logger.error(`${err.stack || err}`);
}
// Handle failure response
else {
logger.error(err);
}
}
}

let recordingAudioFile = 'recordingAudio.opus';

encryptRecording(recordingAudioFile);
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const platformClient = require('purecloud-platform-client-v2');
const winston = require('winston');

const fetch = require('node-fetch');
const fs = require('fs');
const md5 = require('md5-file');
const archiver = require('archiver');

const clientId = process.env.GENESYS_CLOUD_CLIENT_ID;
const clientSecret = process.env.GENESYS_CLOUD_CLIENT_SECRET;

// Set the Genesys Cloud region
const client = platformClient.ApiClient.instance;
client.setEnvironment(platformClient.PureCloudRegionHosts.us_east_1);

// Create API instance
const uploadsApi = new platformClient.UploadsApi();

// Use winston for logging
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(), winston.format.prettyPrint()
),
transports: [
new winston.transports.File({
filename: `./byoiclient-upload-recordings.log`,
}),
// Output to console for error logs
new winston.transports.Console({
level: 'error',
})
]
});


/* Demonstrate the process to upload recordings:
*
* Login OAuth client with client ID and secret
* Create zip with the recording and metadata file
* Load the zip file and create the MD5 hash
* Obtain a presigned URL
* Upload the file to the presigned URL
* Catch and log any error
*/

async function uploadRecordings(fileName) {
try {
let response = await client.loginClientCredentialsGrant(clientId, clientSecret);
logger.verbose('Login successfully');

// >> START byoi-zip-file-cipherv2
var output = fs.createWriteStream(fileName);
var archive = archiver('zip', {
store: true,
// uploaded zip file must be uncompressed, setting compression level to 0
zlib: { level: 0 }
});

archive.on('error', function(err) {
logger.error(err);
});

archive.pipe(output);

// the recording and metadata file must be stored at the root level in the zip file
archive.file('./myfile.opus.bin', {name: 'myfile.opus.bin'});
archive.file('./metadata.json', {name: 'metadata.json'});

// wait for streams to complete
archive.finalize();
// >> END byoi-zip-file-cipherv2
// >> START byoi-get-presigned-url-cipherv2
// Get base64-encoded 128-bit MD5 digest of the file content
let md5sum = await md5(fileName);
let md5Base64 = Buffer.from(md5sum, 'hex').toString('base64');
logger.verbose(md5Base64);

// Get the presigned URL
// signedUrlTimeoutSeconds is optional for the number of seconds the presigned URL is valid for (1-604800).
// The default value is 600 seconds if it is not provided
response = await uploadsApi.postUploadsRecordings({
'fileName': fileName,
'contentMd5': md5Base64,
'signedUrlTimeoutSeconds': 600
});
logger.info(response);
// >> END byoi-get-presigned-url-cipherv2
// Get the presigned URL from the response
let presignedUploadUrl = response.url;
// Save the headers in returned response for the following upload
let responseHeaders = response.headers;
// >> START byoi-upload-file-cipherv2
// Upload the file to the presigned URL
const fileContent = fs.readFileSync(fileName);
logger.info(`Upload ${fileName} to the presigned URL`)
response = await fetch(presignedUploadUrl, {
method: 'PUT',
headers: responseHeaders,
body: fileContent
});
let responseBody = await response.text();
logger.info(responseBody);
return responseBody;
// >> END byoi-upload-file-cipherv2
}
catch(err) {
// Directly logging an error in winston would result in empty string
if (err instanceof Error) {
logger.error(`${err.stack || err}`);
}
// Handle failure response
else {
logger.error(err);
}
}
}

// The recording file to be uploaded
/* Note: The recording file and metadata.json shall be at the top level of the zip file.
Zipping up the folder that contains the recording file and the metadata.json would cause a validation failure

recordingExample.zip
|--- audioExampleRecording.opus.bin
|--- metadata.json
*/

const fileName = 'recordingExample.zip';

uploadRecordings(fileName);
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const winston = require('winston');
const fetch = require('node-fetch');
const fs = require('fs');
const md5 = require('md5-file');
const fs = require('fs');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixing a syntax problem with the existing example

const archiver = require('archiver');

const clientId = process.env.GENESYS_CLOUD_CLIENT_ID;
Expand Down