How to Develop a Native PGP Encryption Suitelet in NetSuite

Orion Correa
OdeBlog
Published in
7 min readApr 19, 2021
Have a 3rd party NetSuite integration that requires native PGP encryption? NetSuite expert Orion Correa has you covered!

NetSuite expert Orion Correa walks us through the process he took to develop a Suitelet integration that allows native PGP encryption for a 3rd party system.

I recently had a client that needed a custom integration to a 3rd party system. There is a connector and an api, but neither supported the data path we needed to handle.

So our solution was to programmatically generate a properly formatted csv file and send it via SFTP to the 3rd party server. From there it would be automatically ingested and processed.

However, the 3rd party requires all transferred files be encrypted with a PGP public/private key pair. NetSuite does not support this natively. The N/crypto module supports symmetrical AES encryption, but not asymmetrical PGP encryption. Possible solutions would be

1. Use a Celigo or similar integration engine which could support the encryption process outside of NetSuite

2. Stand up my own middle-man using AWS to do the same with a node module for example (I’ve done this for other projects)

3. Find some way to import a pgp library to NetSuite to encrypt the files locally

Partially out of stubbornness and partially out of the desire to avoid recurring costs down the line, I decided to explore option 3 to see if it was feasible before going back to the client with the need for a middleman server of some kind.

Developing the Process

Since NetSuite doesn’t support importing node modules or other frameworks, I needed a stand-alone library. And after a long night of googling, I finally found exactly what I needed: the openpgp library. It can be loaded in any common JS framework and is a stand-alone library for PGP encryption.

The next challenge was to figure out how to import it into NetSuite. I tried just manually putting it in the file cabinet and loading it in my scheduled script, but that failed compilation due to file format.

I tried, somewhat lazily, to wrap the entire module in a define statement with a callback that returned the entire library. This did not work either as the library has multiple sections. In retrospect, I suspect this might have been possible if you went through and properly wrapped all the sections to make it AMD compliant.

Finally, I had the idea to load it in an html script tag from a CDN. That eventually worked, and I ended up with a front-end Suitelet that has an inline html field with script tags that do all my logic.

Suitelet behavior:

· Loads file to be encrypted from a file id passed in the url

· Loads key files from file cabinet with hardcoded file ids

· Inline HTML field that loads the openpgp library in a script tag

· In the same HTML field, it then runs another script tag that calls the functions inside the library to read the keys and encrypt the file contents

· Then copies that encrypted contents to a text area on the field

· Then, after a short setTimeout to make the timing work, calls submit on the form on the page

· In the submit logic, it grabs the contents of the text area and saves it to a new file

· Then redirects to itself with the id of the encrypted file in the url

· This time the script sees the new url param with the output file id and instead of rendering a page, it returns a simple message with the output file id

Conclusion

I believe this is a useful technique for when you need to load a library for something not natively supported in NetSuite SuiteScript libraries. It is a very modular process and can be fired off with a single click from any menu link, form button, or UI page.

The biggest caveat is that it does require the Suitelet to actually be rendered in a browser window, so it cannot be used in a purely backend script. For this use case, the client will have to manually click a menu link to fire off the process; considering, however, it only takes around 3 seconds to run and only needs to be run once a week, it’s a good solution to avoid a middleman server.

This can be done similarly for decryption simply by loading the proper keys and specifying so in the input.

/**
*
* Version Date Author Remarks
* 1.00 ocorrea Initial version
*
* @NApiVersion 2.x
* @NScriptType Suitelet
*/
define(['N/record', 'N/search','N/ui/serverWidget', 'N/file', 'N/redirect', 'N/sftp' ], function (record, search,ui, file, redirect, sftp ) {

function onRequest(context) {
if (context.request.method === 'GET') {
log.debug('fileId', context.request.parameters)

if (context.request.parameters.fileId) {
context.response.write({
// output: JSON.stringify(ret)
output: "File saved and submitted. Id: "+context.request.parameters.fileId
})
}
else {
var form = ui.createForm({
title: 'PGP encryption',
id: 'encryptionform'
});

var pgroup = form.addFieldGroup({
id : 'pgroup',
label : 'Params'
});
var pubkeyFile = file.load({id: 222}); // public key
var pubKeyText = pubkeyFile.getContents();

var prikeyFile = file.load({id: 333});
var priKeyText = prikeyFile.getContents();

var encryptFileId = generateConcurFile() //generating csv to be encrypted
if (encryptFileId && encryptFileId > 0) {
var fileToEncrypt = file.load({id: encryptFileId})
}
var decryptFileId = context.request.parameters.decryptFileId || -1
if (decryptFileId && decryptFileId > 0) {
var fileToDecrypt = file.load({id: decryptFileId})
}

form.addSubmitButton({
label: 'Submit'
});
var resultField = form.addField({
id: 'encryptresult',
label: "Results",
type: ui.FieldType.TEXTAREA ,
container: 'pgroup'
});

if (fileToDecrypt) {
var htmlField = form.addField({
id: 'singleempid',
label: "InlineHTML",
type: ui.FieldType.INLINEHTML ,
container: 'pgroup'
}).defaultValue =
'<script src="https://cdnjs.cloudflare.com/ajax/libs/openpgp/2.5.11/openpgp.js"></script>' +
'<script>var foobar = typeof openpgp; console.log("openpgp decrypt"+foobar);</script>'+

'<script>'+
'(async () => {'+
'const publicKeyArmored = `' + pubKeyText + '`;'+
'const privateKeyArmored = `' + priKeyText + '`;'+
// 'console.log("after keys"); '+
// 'const passphrase = ``; '+
'const { keys: [publicKey] } = await openpgp.key.readArmored(publicKeyArmored);'+
// 'console.log("after publicKey",publicKey); '+
'const { keys: [privateKey] } = await openpgp.key.readArmored(privateKeyArmored);'+
// 'console.log("after privatekey",privateKey); '+
// 'const message = "Hello, World!";'+
// 'console.log("message",message); '+
// 'await privateKey.decrypt(passphrase);'+
// 'const {data: encrypted} = await openpgp.encrypt({data: message, publicKeys: publicKey, privateKeys: privateKey });'+
// 'console.log(encrypted); '+
'const encrypted = `'+fileToDecrypt.getContents()+'`;'+
'const { data: decrypted } = await openpgp.decrypt({message: await openpgp.message.readArmored(encrypted),publicKeys: publicKey, privateKey: privateKey });'+
// 'console.log(decrypted); '+
'jQuery("#encryptresult").val(encrypted);'+
'setTimeout(function(){jQuery("#main_form").submit();}, 1000);'+
'console.log("after submit");'+
'})();</script>'

}
else if (fileToEncrypt) {
var htmlField = form.addField({
id: 'singleempid',
label: "InlineHTML",
type: ui.FieldType.INLINEHTML ,
container: 'pgroup'
}).defaultValue =
'<script src="https://cdnjs.cloudflare.com/ajax/libs/openpgp/2.5.11/openpgp.js"></script>' +
'<script>var foobar = typeof openpgp; console.log("openpgp encrypt"+foobar);</script>'+

'<script>'+
'(async () => {'+
'const publicKeyArmored = `' + pubKeyText + '`;'+
'const privateKeyArmored = `' + priKeyText + '`;'+
'console.log("after keys"); '+
// 'const passphrase = ``; '+
'const { keys: [publicKey] } = await openpgp.key.readArmored(publicKeyArmored);'+
'console.log("after publicKey",publicKey); '+
'const { keys: [privateKey] } = await openpgp.key.readArmored(privateKeyArmored);'+
'console.log("after privatekey",privateKey); '+
'const message = `'+fileToEncrypt.getContents()+'`;'+
// 'console.log("message",message); '+
// 'await privateKey.decrypt(passphrase);'+
'const {data: encrypted} = await openpgp.encrypt({data: message, publicKeys: publicKey, privateKeys: privateKey });'+
'console.log(encrypted); '+
'jQuery("#encryptresult").val(encrypted);'+
// 'setTimeout(function(){jQuery("#main_form").submit();}, 1000);'+
// 'console.log("after submit");'+

'})();</script>'

}

context.response.writePage(form);
}

} else {
// log.debug ('SUBMIT Handler', context.request.parameters);
// log.debug('submited result', context.request.parameters.encryptresult)
// create file, return
var dsTime = Date.now();
var hostKey = 'hostkey';
var url = 'url';
var username = 'username';
var fileId = file.create({
name: "pgpfile_"+username+"_"+getDateString(),
fileType: file.Type.PLAINTEXT,
contents: context.request.parameters.encryptresult,
folder: 12345
}).save()


try {
var connection = sftp.createConnection({
username: username,
keyId: 'cust_keyid',
url: url,
hostKey: hostKey,
hostKeyType: 'rsa',
directory: '/'
});
var dirList = connection.list({
path: ''
})
log.debug('dirList',dirList)

}
catch (e) {
log.error('error making connection', e)
}

redirect.toSuitelet({
scriptId: 'customscript112',
deploymentId: 'customdeploy1',
parameters: {fileId: fileId}
});
}
}

function generateConcurFile() {

//run saved search for expense reports to send
//for each one add line to csv file for concur (check example file)
//if remaining governance < 100, break loop,
//call encrypt suitelet and push to server

try {
// Get Paramters
var username = 'username';


var csvContents = ""
var expenseReportSearch = search.load({id: 'customsearch1'}).run().each(function(result) {
var amount = result.getValue('amount') * 100
var date = result.getValue('date')
var dateStr = getISODateString(date)
var tranId = result.getValue('tranid')
var currency = result.getText('currency')
var checkNum = result.getValue({join: 'applyingTransaction', name: 'tranid'})

csvContents += "600,"+amount+","+dateStr+",,,"+checkNum+","+tranId+","+currency+",,,,,\r\n"
return true;
})
if (csvContents.length > 0) {
csvContents = "100,LF,ID\r\n"+csvContents
log.debug('csvcontents',csvContents)
var fileId = file.create({
name: "exp_pay_confirm_"+username+"_"+getDateString(),
fileType: file.Type.PLAINTEXT,
contents: csvContents,
folder: 12345
}).save()
return fileId

}


} catch (e) {
log.error({
title: e.name,
details: e.message
});
} // End Try

} // End Execute

function getISODateString(today) {
if (typeof today !== 'date') {
today = new Date();
}
var year = today.getFullYear();
var month = +today.getMonth() + 1;
if (month < 10) month = "0"+month;
var date = +today.getDate();
if (date < 10) date = "0" + date;

return String(year+month+date)
}
function getDateString(today) {
if (typeof today !== 'date') {
today = new Date();
}
var year = today.getFullYear();
var month = +today.getMonth() + 1;
if (month < 10) month = "0"+month;
var date = +today.getDate();
if (date < 10) date = "0" + date;

return String(month+date+year)
}

function isEmpty (stValue)
{
if ((stValue === '') || (stValue == null) || (stValue == undefined))
{
return true;
}
else
{
if (typeof stValue == 'string')
{
if ((stValue == ''))
{
return true;
}
}
else if (typeof stValue == 'object')
{
if (stValue.length == 0 || stValue.length == 'undefined')
{
return true;
}
}

return false;
}
}

return {
onRequest: onRequest
};
})

--

--