Overview - Senetas/SKC GitHub Wiki

Introduction

The Senetas Key Orchestrator (SKC) is a stand alone, stateless key orchestration solution designed to support SureDrop in multiple Key Management Solutions (KMS).

While SKC is designed to support SureDrop, it can also be used as a stand alone solution for encryption/decryption of arbitrarily large data using a standardized and modern REST API by abstracting and standardizing multiple back end Key Management Solutions.

Senetas Key Orchestrator (SKC) should be used in the following situations:

  1. When Suredrop needs to connect to multiple KMS's with failover and redundancy
  2. When Suredrop needs to connect to a KMS in a geographically different region
  3. Where there is high network latency between SureDrop and the KMS.
  4. Where KMS availability may not be 100%
  5. Where KMS performance is an issue
  6. Where SureDrop needs to connect to KMS's from different vendors at the same time.
  7. When a high performance, stateless encryption/decryption solution is required.
  8. When SureDrop needs to connect to a Luna HSM

OS Support

SKC is written in dotnetcore version 3.1 and supports the following platforms:

  1. Windows
  2. MacOS
  3. Unix

Each platform has differences as follows:

  1. Windows
    The Windows version supports SKC running as a windows service.
    The Windows version supports the Windows Event Log.

  2. Unix
    The Unix version does not support Luna HSM.

  3. MacOS
    The MacOS version does not support Luna HSM.

Overview

The Senetas Key Orchestrator operates as follows:

Providers

Currently SKC supports the following providers:

  1. Amazon KMS
  2. Senetas Key Vault (with support of IDQ Quantis Quantum Random Number Generator)
  3. Thales SafeNet Luna HSM
  4. Thales KeySecure (Classic & Nextgen)
  5. SKC also support itself as a provider.

Key Encryption Keys (KEK)

Back end Providers are configured in SKC as Key Encryption Key (KEK) providers.

There may be multiple back end providers configured at any time.

All providers are used for the encrypt action, but only one provider is required for the decrypt action.

The role of the provider is to take a Data Encryption Key (DEK) and to encrypt/decrypt it using the KEK.

Providers are configured with a priority similar to mail servers. Providers will be tried in order of lowest network latency within the same priority group during a decrypt action. Once all providers have been exhausted within a priority group and a successful KEK solution has not been determined to decrypt the DEK, the next level of priority will be searched.

Providers provide named KEK's, and also provide a wildcard 'default' key if no key is named during the encrypt/decrypt action.

Data Encryption Key (DEK)

The Data Encryption Key (DEK) is used to encrypt the actual data. The DEK is generated by one of three means:

  1. If any of the configured providers support a cryptographically secure random number generator, this will be used to generate the DEK and add it to the random pool.

  2. If none of the providers support a random function, then a DEK will be generated internally using the RNGCryptoServiceProvider provided by microsoft and added to the random pool.

  3. A DEK is reused from the random pool. The number of times a DEK can be used is configured globally for the SKC but can be overridden in each request. While SKC can be configured to use a unique DEK for every request, this can substantially reduce performance. For high performance it is recommended that a new DEK should not be generated more than once a second.

  eg. For an average of 1000/actions per second a limit of 
      1000 DEK key reuse should be configured.

Encryption walk though step by step

  1. Plaintext data is received via the encrypt action REST API POST.
    (Data may be binary, JSON or plaintext)

  2. The data is compressed using standard GZip compression.

  3. A DEK is generated.

  4. The in memory cache is then checked to determine if a ciphertext version of the DEK exists for each provider. If a ciphertext version of the DEK already exists then the provider is not queried and the encrypted DEK is returned immediately.

  5. If the encrypted DEK does not already exist in the cache, copy of the plaintext DEK is sent to all of the providers in parallel threads requesting that it be encrypted with each individual KEK.

  6. Multiple copies of the encrypted DEK are returned. Each copy of the DEK is named with the provider id and stamped with the timestamp of when it was encrypted and a TTL DEK expiry, it is then converted to base64.

  7. The compressed Plaintext is encrypted using BouncyCastle with the following parameters:

  The plaintext DEK (key) and the Username (nonSecretPayload)  
public byte[] EncryptWithKey(byte[] messageToEncrypt, byte[] key, byte[] nonSecretPayload = null)
        {
            //User Error Checks
            if (key == null || key.Length != KEY_BIT_SIZE / 8)
            {
                throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
            }

            //Non-secret Payload Optional
            nonSecretPayload = nonSecretPayload ?? new byte[] { };

            //Using random nonce large enough not to repeat
            var nonce = new byte[NONCE_BIT_SIZE / 8];
            _random.NextBytes(nonce, 0, nonce.Length);

            var cipher = new GcmBlockCipher(new AesFastEngine());
            var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce, nonSecretPayload);
            cipher.Init(true, parameters);

            //Generate Cipher Text With Auth Tag
            var cipherText = new byte[cipher.GetOutputSize(messageToEncrypt.Length)];
            var len = cipher.ProcessBytes(messageToEncrypt, 0, messageToEncrypt.Length, cipherText, 0);
            cipher.DoFinal(cipherText, len);

            //Assemble Message
            using (var combinedStream = new MemoryStream())
            {
                using (var binaryWriter = new BinaryWriter(combinedStream))
                {
                    //Prepend Authenticated Payload
                    binaryWriter.Write(nonSecretPayload);
                    //Prepend Nonce
                    binaryWriter.Write(nonce);
                    //Write Cipher Text
                    binaryWriter.Write(cipherText);
                }
                return combinedStream.ToArray();
            }
        }
  1. The result is also converted to base64.

  2. The multiple copies of the encrypted DEK and the ciphertext are combined into a JSON structure with the following format:

{
    "keys": 
    [
        "AAAAAAA==",
        "BBBBBBB==",
        ...
    ],
    "data": "DDDDDDD=="
}
  1. This is either returned as a bas64 blob or as a JSON string depending on the input parameters.

Decryption walk though step by step

  1. The base64 encoded blob or JSON snippet that was returned from the encrypt action is fed back to the SKC decrypt action via a POST and the decrypted keys and data identified.

  2. The lookup memory cache of SKC is queried to determine if the plaintext DEK exists in the cache. If it is, no providers are queried and the plaintext DEK is returned from memory immediately.

  3. If the plaintext DEK is not found in the cache, SKC then lists each of the back end KMS providers in order of priority as defined when the provider was created. If two providers have the same priority a small random jitter is introduced to ensure that each provider of the same priority is randomly selected each time.

  4. Each provider is then sent it's matching (matched by id) encrypted DEK key in turn to be decrypted by the providers KEK if it exists in the list of encrypted keys, if the key does not exist it is skipped.

  5. If a provider is able to return the decrypted DEK, then no more providers are queried.

  6. The decrypted DEK is then used to decrypt the data ciphertext using the following function:

  The plaintext DEK (key) and the Username (nonSecretPayload)  
public byte[] DecryptWithKey(byte[] encryptedMessage, byte[] key, byte[] nonSecretPayload = null)
        {
            int nonSecretPayloadLength = nonSecretPayload.Length;

            //User Error Checks
            if (key == null || key.Length != KEY_BIT_SIZE / 8)
            {
                throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
            }

            if (encryptedMessage == null || encryptedMessage.Length == 0)
            {
                throw new ArgumentException("Encrypted Message Required!", "encryptedMessage");
            }

            using (var cipherStream = new MemoryStream(encryptedMessage))
            using (var cipherReader = new BinaryReader(cipherStream))
            {
                //Grab Payload
                var nonSecretPayloadMsg = cipherReader.ReadBytes(nonSecretPayloadLength);

                if (nonSecretPayload.FromBytes() != nonSecretPayloadMsg.FromBytes())
                {
                    throw new Exception("Non Secret Payload Does not Match!");
                }

                //Grab Nonce
                var nonce = cipherReader.ReadBytes(NONCE_BIT_SIZE / 8);

                var cipher = new GcmBlockCipher(new AesFastEngine());
                var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce, nonSecretPayload);
                cipher.Init(false, parameters);

                //Decrypt Cipher Text
                var cipherText = cipherReader.ReadBytes(encryptedMessage.Length - nonSecretPayloadLength - nonce.Length);
                var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];

                try
                {
                    var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
                    cipher.DoFinal(plainText, len);
                }
                catch (InvalidCipherTextException)
                {
                    //Return null if it doesn't authenticate
                    return null;
                }

                return plainText;
            }
        }
  1. The resulting plaintext is then decompressed using GZip and returned.