Thursday, 6 June 2024

PGP Encryption and Digital signature in Dynamics 365 Finance and Operations for payment files

In this blog post I am going to discuss about Digital signature and PGP encryption for txt/xml data. This is generally required for payment processing files being sent out of D365 FO. 

As a pre-requisite, you must have a PGP encrypted public key in format as

-----BEGIN PGP PUBLIC KEY BLOCK-----

key content

-----END PGP PUBLIC KEY BLOCK-----


, a private key (if you don't have, then generate it using kleopatra tool) in format as 

-----BEGIN PGP ARMORED FILE-----

key content

-----END PGP ARMORED FILE-----


and a passphrase for private key. 

These keys and passphrase must be securely stored. I have stored it over Azure key vault and is using D365 Key vault parameters to retrieve that using azure client app. (for Dev box, Same is possible via txt files stored locally too, Or as a resource file for Dev and other environments, Or as a txt file over azure storage account).

Although its the safest way to use azure key vault, but there is a minor problem as to how the whole key is stored. Generally key must include special characters '\r\n' (Not directly visible unless read via code or using tools such as notepad++), when pasting it over key vault it ignores the new lines and considers whole data as a single line. To overcome this, do use the line ending characters before pasting it to key vault as

-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: PGP Command Line v10.1.0 (Build 52) (Linux)\r\n\r\nkey content\r\n-----END PGP PUBLIC KEY BLOCK----

Once you are finalized with all these, lets proceed for code. 

We will start with the PGP helper class/

 

using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Bcpg;

class PGPHelper
{
   /// 
/// SignFile
/// 
/// System.IO.Stream
/// System.IO.Stream
/// str
/// System.IO.Stream
public System.IO.Stream SignFile(System.IO.Stream _inputFile, System.IO.Stream _privateKeyStream, Str _passphrase)
{
    PgpSecretKey secretKey = this.ReadSecretKey(_privateKeyStream);

    System.Byte[] passPhraseByte = System.Text.Encoding::UTF8.GetBytes(_passphrase);
    System.Char[] passPhraseChar = System.Text.Encoding::UTF8.GetChars(passPhraseByte);
    PgpPrivateKey privateKey = secretKey.ExtractPrivateKey(passPhraseChar);

    System.IO.Stream outputStream = new System.IO.MemoryStream();
    PgpSignatureGenerator pgpSignatureGenerator = new PgpSignatureGenerator(privateKey.PublicKeyPacket.Algorithm, Org.BouncyCastle.Bcpg.HashAlgorithmTag::Sha256);
    pgpSignatureGenerator.InitSign(0, privateKey);

    PgpLiteralDataGenerator literalData = new PgpLiteralDataGenerator();
    System.Char format = Global::toanytype('b');
    var literalOut = literalData.Open(outputStream, format, "filename", _inputFile.Length, DateTimeUtil::utcNow());

    var signatureOut = pgpSignatureGenerator.GenerateOnePassVersion(false);
    signatureOut.Encode(literalOut);

    _inputFile.CopyTo(literalOut);

    var signature = pgpSignatureGenerator.Generate();
    signature.Encode(literalOut);

    literalOut.Close();
    outputStream.Position = 0;

    return outputStream;
}

/// 
/// SignAndEncryptData
/// 
/// System.IO.Stream
/// System.IO.Stream
/// str
/// System.byte
/// system.byte
public System.Byte[] SignAndEncryptData(System.IO.Stream _publicKeyFileStream, System.IO.Stream _secretKeyFileStream, Str _privateKeyPassword, System.Byte[] _messageDataArray)
{
    try
    {
        PgpPublicKey pubKey = this.ReadPublicKey(_publicKeyFileStream);
        PgpSecretKey secretKey = this.ReadSecretKey(_secretKeyFileStream);

        System.Byte[] passPhraseByte = System.Text.Encoding::UTF8.GetBytes(_privateKeyPassword);
        System.Char[] passPhraseChar = System.Text.Encoding::UTF8.GetChars(passPhraseByte);

        using (var outputStream = new System.IO.MemoryStream())
        {
            this.SignAndEncrypt(outputStream, _messageDataArray, secretKey, passPhraseChar, pubKey, true, true);
            return outputStream.ToArray();
        }
    }
    catch
    {
        throw error('Error during sign and encrypt process');             
    }
}

private void SignAndEncrypt(System.IO.Stream _outputStream, System.Byte[] _clearData, PgpSecretKey _secretKey, System.Char[] _secretPwd, PgpPublicKey _publicKey, Boolean _armored, Boolean _withIntegrityCheck)
{
    if (_armored)
    {
        _outputStream = new ArmoredOutputStream(_outputStream);
    }

    try
    {
        PgpEncryptedDataGenerator encryptedDataGenerator = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag::Cast5, _withIntegrityCheck, new SecureRandom());
        encryptedDataGenerator.AddMethod(_publicKey);
        System.Byte[] buffer = new System.Byte[1 << 16]();

        using (System.IO.Stream encryptedOut = encryptedDataGenerator.Open(_outputStream, buffer))
        {
            var compressedDataGenerator = new PgpCompressedDataGenerator(CompressionAlgorithmTag::Uncompressed);
            using (System.IO.Stream compressedOut = compressedDataGenerator.Open(encryptedOut))
            {
                PgpPrivateKey privateKey = _secretKey.ExtractPrivateKey(_secretPwd);
                var signatureGenerator = new PgpSignatureGenerator(_secretKey.PublicKey.Algorithm, HashAlgorithmTag::Sha256);                    
                signatureGenerator.InitSign(0, privateKey);
                
                System.Collections.IEnumerable enumUserLiterals = _secretKey.PublicKey.GetUserIds();

                //str userId;enumUserLiterals.getnext();
                System.Collections.IEnumerator inumerator = enumUserLiterals.GetEnumerator();

                while (inumerator.moveNext())
                {
                    PgpSignatureSubpacketGenerator subpacketGenerator = new PgpSignatureSubpacketGenerator();
                    //subpacketGenerator.AddSignerUserId(false, userId);
                    //subpacketGenerator.SetIssuerFingerprint(false, _secretKey.PublicKey);
                    subpacketGenerator.SetSignerUserId(false, inumerator.Current);
                    subpacketGenerator.SetIssuerKeyID(false, _secretKey.PublicKey.KeyId);
                    signatureGenerator.SetHashedSubpackets(subpacketGenerator.Generate());
                    break;
                }

                signatureGenerator.GenerateOnePassVersion(false).Encode(compressedOut);

                var literalDataGenerator = new PgpLiteralDataGenerator();
                System.Char binaryformat = Global::toanytype('b');
                using (System.IO.Stream literalOut = literalDataGenerator.Open(compressedOut, binaryformat , "embeddedFileName", _clearData.Length, DateTimeUtil::utcNow()))
                {
                    using (System.IO.Stream inputData = new System.IO.MemoryStream(_clearData))
                    {
                        int ch = inputData.ReadByte();
                        while (ch >= 0)
                        {
                            //literalOut.WriteByte((byte)ch);
                            //signatureGenerator.Update((byte)ch);
                            literalOut.WriteByte(ch);
                            signatureGenerator.Update(ch);

                            ch = inputData.ReadByte();
                        }
                    }
                }

                signatureGenerator.Generate().Encode(compressedOut);
            }
        }

        if (_armored)
        {
            _outputStream.Close();
        }
    }
    catch
    {
        throw error("Error occurred during sign and encrypt process.");
    }
}

private PgpPublicKey ReadPublicKey(System.IO.Stream _publicKeyFileStream)
{
    System.IO.Stream decodedStream  = PgpUtilities::GetDecoderStream(_publicKeyFileStream);
    PgpPublicKeyRingBundle pgpPub = new PgpPublicKeyRingBundle(decodedStream);
    PgpPublicKeyRing kRing;
    System.Collections.IEnumerator kRingenumerator = pgpPub.GetKeyRings().GetEnumerator();
    while (kRingenumerator.MoveNext())
    {
        kRing = kRingenumerator.Current;
        PgpPublicKey k;
        System.Collections.IEnumerator kenumerator = kRing.GetPublicKeys().GetEnumerator();
        while (kenumerator.MoveNext())
        {
            k = kenumerator.Current;
            if (k.IsEncryptionKey)
            {
                return k;
            }
        }
    }

    throw error('No encryption key found in public key ring.');
}

private PgpSecretKey ReadSecretKey(System.IO.Stream _secretFileKey)
{
    System.IO.Stream decodedStream = PgpUtilities::GetDecoderStream(_secretFileKey);
    PgpSecretKeyRingBundle pgpSec = new PgpSecretKeyRingBundle(decodedStream);

    PgpSecretKey secretKey;
    PgpSecretKeyRing kRing;
    System.Collections.IEnumerator kRingenumerator = pgpSec.GetKeyRings().GetEnumerator();
    while (kRingenumerator.MoveNext())
    {
        kRing = kRingenumerator.Current;

        PgpSecretKey k;
        System.Collections.IEnumerator kenumerator = kRing.GetSecretKeys().GetEnumerator();
        while (kenumerator.MoveNext())
        {
            k = kenumerator.Current;
            if (k.IsSigningKey)
            {
                secretKey = k;
                break;
            }
        }

        if (secretKey != null)
        {
            break;
        }
    }

    if (secretKey == null)
    {
        throw error('No signing key found in secret key ring.');
    }
    
   
   
    return secretKey;
}

/// 
/// EncryptFile
/// 
/// System.IO.Stream
/// System.IO.Stream
/// System.IO.Stream
public System.IO.Stream EncryptFile(System.IO.Stream _inputFile, System.IO.Stream _publicKeyStream)
{
    PgpPublicKey publicKey = this.ReadPublicKey(_publicKeyStream);
    System.IO.MemoryStream outputStream = new System.IO.MemoryStream();

    // Armored output
    ArmoredOutputStream armoredStream = new ArmoredOutputStream(outputStream);

    // Encryption generator
    PgpEncryptedDataGenerator encryptedData = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag::Cast5, true, new SecureRandom());
    encryptedData.AddMethod(publicKey);

    using (System.IO.Stream encryptedOut = encryptedData.Open(armoredStream, new System.Byte[1 << 16]()))
    {
        // Optional compression
        PgpCompressedDataGenerator compressedData = new PgpCompressedDataGenerator(CompressionAlgorithmTag::Uncompressed);
        using (System.IO.Stream compressedOut = compressedData.Open(encryptedOut))
        {
            // Literal data
            PgpLiteralDataGenerator literalData = new PgpLiteralDataGenerator();
            System.Char binaryformat = Global::toanytype('b');
            using (System.IO.Stream literalOut = literalData.Open(compressedOut, binaryformat, "data", _inputFile.Length, DateTimeUtil::utcNow()))
            {
                _inputFile.CopyTo(literalOut);
            }
        }
    }

    armoredStream.Close();
    outputStream.Position = 0;
    return outputStream;
}
}

Code can be modified if compression is needed. (I have it commented, just uncomment it if needed).
Next to use the helper class to do the actual encryption and signature. 

We can read the file contents to be encrypted or use the data generated at runtime using io.stream.


internal final class RunnableClass1
{
	public static void main(Args _args)
    {
        KeyVaultParameters          keyVaultParameters;
                KeyVaultCertificateTable    certTable;
                //FinTechDotNetLib.PgpHelper pgpHelper = new FinTechDotNetLib.PgpHelper();
                PGPHelper   pgpHelper = new PGPHelper();
                System.IO.Stream encryptedStream;
                System.IO.Stream signedStream;
                
         select keyVaultParameters
            where  keyVaultParameters.Name = 'keyVaulttest';
            
            System.IO.Stream streamName = new System.IO.MemoryStream(System.Text.Encoding::UTF8.GetBytes(System.IO.File::ReadAllText(@"D:\Test.xml")));
            
              str publicKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('publickey').RecId);
              
               str privateKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('privatekey').RecId);
               
                 str formattedPublicKey;
  formattedPublicKey = strReplace(publicKey, "\\r\\n", '\r\n');

  str formattedPrivateKey;
  formattedPrivateKey = strReplace(privateKey, "\\r\\n", '\r\n');
              
 System.IO.Stream publicKeyStream    = new System.IO.MemoryStream(System.Text.Encoding::UTF8.GetBytes(formattedPublicKey));
 System.IO.Stream privateKeyStream   = new System.IO.MemoryStream(System.Text.Encoding::UTF8.GetBytes(formattedPrivateKey));
 
  if (keyVaultParameters.DigitalSignature && keyVaultParameters.Encryption) //These are custom No/Yes field
                {
                    System.Byte[] signedAndEncryptedByte = pgpHelper.SignAndEncryptData(publicKeyStream, privateKeyStream, KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('passphrase').RecId), dataByte);
                     signedAndEncryptedStream = new System.IO.MemoryStream(signedAndEncryptedByte);
                }
                else if (keyVaultParameters.DigitalSignature && !keyVaultParameters.Encryption) //This is custom No/Yes field
                {
                    signedStream = pgpHelper.SignFile(streamName, formattedPrivateKey, KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('passphrase').RecId));
                }              
                else if (keyVaultParameters.Encryption && !keyVaultParameters.DigitalSignature) //These are custom No/Yes field
                {
                    encryptedStream = pgpHelper.EncryptFile(streamName, formattedPublicKey);
                }
                
                if(keyVaultParameters.DigitalSignature && keyVaultParameters.Encryption)
                {
                	System.IO.StreamReader streamReader = new System.IO.StreamReader(signedAndEncryptedStream);
                	info (streamReader.ReadToEnd());
                }
                else if (keyVaultParameters.DigitalSignature && !keyVaultParameters.Encryption)
                {
                    System.IO.StreamReader streamReader = new System.IO.StreamReader(signedStream);
                	info (streamReader.ReadToEnd());
                }
                else
                {
                      System.IO.StreamReader streamReader = new System.IO.StreamReader(encryptedStream);
                      info (streamReader.ReadToEnd());
                }
    }
}

Helper class can also be designed using C# and .Net assembly can then be used to pass the file stream and key strings, but its just easier to manage that with x++ and is easy to debug in case of any issues. 

Below is equivalent C# Code for helper if  needed.
using System;
using System.Linq;
using System.Text;
using System.IO;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;

namespace FinTechDotNetLib
{
    public class PgpHelper
    {
        public Stream EncryptFile(Stream inputFile, string publicKeyString)
        {
            PgpPublicKey publicKey = ReadPublicKey(publicKeyString);

            MemoryStream outputStream = new MemoryStream();

            PgpEncryptedDataGenerator encryptedDataGenerator = new PgpEncryptedDataGenerator(Org.BouncyCastle.Bcpg.SymmetricKeyAlgorithmTag.Cast5, true, new SecureRandom());
            encryptedDataGenerator.AddMethod(publicKey);

            using (Org.BouncyCastle.Bcpg.ArmoredOutputStream armoredOut = new Org.BouncyCastle.Bcpg.ArmoredOutputStream(outputStream))
            {
                // Open the encrypted stream
                using (Stream encryptedOut = encryptedDataGenerator.Open(armoredOut, new byte[1 << 16]))
                {
                    //PgpCompressedDataGenerator compressedDataGenerator = new PgpCompressedDataGenerator(Org.BouncyCastle.Bcpg.CompressionAlgorithmTag.Uncompressed);
                    //Stream compressedOut = compressedDataGenerator.Open(encryptedOut);

                    // Write the input file data to the encrypted stream
                    byte[] buffer = new byte[1 << 16];
                    int len;
                    while ((len = inputFile.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        encryptedOut.Write(buffer, 0, len);
                    }
                }
            }

            outputStream.Position = 0;

            return outputStream;
        }

        public Stream SignFile(Stream inputFile, string privateKeyString, string passphrase)
        {
            PgpPrivateKey privateKey = ReadPrivateKey(privateKeyString, passphrase);

            MemoryStream outputStream = new MemoryStream();

            PgpSignatureGenerator signatureGenerator = new PgpSignatureGenerator(Org.BouncyCastle.Bcpg.PublicKeyAlgorithmTag.RsaGeneral, Org.BouncyCastle.Bcpg.HashAlgorithmTag.Sha256);
            signatureGenerator.InitSign(PgpSignature.BinaryDocument, privateKey);

            PgpLiteralDataGenerator literalDataGenerator = new PgpLiteralDataGenerator();
            Stream literalOut = literalDataGenerator.Open(outputStream, PgpLiteralData.Binary, "filename", inputFile.Length, DateTime.UtcNow);

            byte[] buffer = new byte[1 << 16];
            int len;
            while ((len = inputFile.Read(buffer, 0, buffer.Length)) > 0)
            {
                literalOut.Write(buffer, 0, len);
                signatureGenerator.Update(buffer, 0, len);
            }

            //signatureGenerator.Generate().Encode(literalOut);
            PgpSignature signature = signatureGenerator.Generate();

            // Write the signature to the output stream
            using (Stream signatureStream = new MemoryStream())
            {
                signature.Encode(signatureStream);
                signatureStream.Position = 0;
                signatureStream.CopyTo(outputStream);
            }

            literalOut.Close();            

            outputStream.Position = 0;

            return outputStream;
        }

        private PgpPublicKey ReadPublicKey(string publicKeyString)
        {
            using (Stream keyIn = new MemoryStream(Encoding.UTF8.GetBytes(publicKeyString)))
            using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))
            {
                PgpPublicKeyRingBundle publicKeyRingBundle = new PgpPublicKeyRingBundle(inputStream);
                foreach (PgpPublicKeyRing keyRing in publicKeyRingBundle.GetKeyRings())
                {
                    foreach (PgpPublicKey key in keyRing.GetPublicKeys())
                    {
                        if (key.IsEncryptionKey)
                        {
                            return key;
                        }
                    }
                }
            }
            throw new ArgumentException("Can't find encryption key in key ring.");
        }

        private PgpPrivateKey ReadPrivateKey(string privateKeyString, string passphrase)
        {
            using (Stream keyIn = new MemoryStream(Encoding.UTF8.GetBytes(privateKeyString)))
            using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))
            {
                PgpSecretKeyRingBundle secretKeyRingBundle = new PgpSecretKeyRingBundle(inputStream);
                foreach (PgpSecretKeyRing keyRing in secretKeyRingBundle.GetKeyRings())
                {
                    foreach (PgpSecretKey key in keyRing.GetSecretKeys())
                    {
                        if (key.IsSigningKey)
                        {
                            PgpPrivateKey privateKey = key.ExtractPrivateKey(passphrase.ToCharArray());
                            if (privateKey != null)
                            {
                                return privateKey;
                            }
                        }
                    }
                }
            }
            throw new ArgumentException("Can't find signing key in key ring.");
        }
    }
}

Thanks for reading! Happy coding !!