Partner Data Decryption Guide
This document outlines the process and technical specifications for partners to decrypt sensitive data received from our systems. To ensure the highest level of data security, sensitive information is encrypted in transit (e.g., via Pub/Sub). This guide provides the necessary information to implement the decryption process on your end, with a strong focus on the underlying cryptographic principles, making it applicable across various programming languages and environments. A complete code example is provided for Kotlin to illustrate the implementation.
1. Overview of the Encryption Scheme
Our encryption mechanism employs industry-standard algorithms to protect data confidentiality and integrity. The core components of our encryption scheme are:
Algorithm: Advanced Encryption Standard (AES) in Galois/Counter Mode (GCM) with No Padding (
AES/GCM/NoPadding). AES-GCM provides both confidentiality and authenticated encryption, ensuring that the data has not been tampered with.Key Derivation: The encryption key is derived from a shared secret password and a unique salt using PBKDF2WithHmacSHA256. This process strengthens the key against brute-force attacks.
Encrypted Data Structure: The final encrypted payload, transmitted as a Base64-encoded string, is a concatenation of three essential components:
Initialization Vector (IV): A 12-byte (96-bit) cryptographically secure random number, unique for each encryption operation.
Salt: A 16-byte (128-bit) unique random value used during key derivation.
Ciphertext: The actual encrypted data, which includes the 16-byte (128-bit) GCM authentication tag appended by the AES/GCM cipher.
2. Secure Provisioning of Credentials
To decrypt the data, you will require a password and a salt. These credentials are provided to you securely:
Password and Salt Delivery: Your dedicated developer contact will provide these credentials. We will securely deliver your password and salt via a one-time link, leveraging tools like Bitwarden Send, or through a pre-arranged secure channel.
Security Best Practices: It is paramount that these credentials are treated with the utmost confidentiality. They should be stored securely and never hardcoded directly into your applications or exposed in logs.
3. Decryption Process
The decryption process involves several steps to reconstruct the original plaintext from the received encrypted data. The principles apply universally, and you should adapt them to your chosen programming language and cryptographic library. A detailed example is provided for Kotlin.
Prerequisites
Before you begin, ensure your environment has:
A compatible runtime environment: This could be any programming language runtime (e.g., Python, C#, Java, Go) that has access to cryptographic capabilities.
A robust cryptographic library: Your chosen library must support the following:
AES/GCM (Advanced Encryption Standard in Galois/Counter Mode)
PBKDF2WithHmacSHA256 (Password-Based Key Derivation Function 2 with HMAC-SHA256)
Base64 encoding/decoding
For JVM-based languages like Kotlin, the Bouncy Castle Crypto API is highly recommended for enhanced cryptographic capabilities.
Example: Gradle Dependency for Bouncy Castle (Kotlin):
dependencies { implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' // Use the latest stable version }
Decryption Steps (Conceptual)
Follow these conceptual steps to decrypt the received ciphertext.
Obtain the Encrypted Data: You will receive the encrypted data as a Base64-encoded string.
Base64 Decode the Ciphertext: Decode the Base64 string back into its raw byte array (or equivalent binary data structure) form.
Extract IV, Salt, and Actual Ciphertext: The decoded byte array contains the IV, Salt, and the actual ciphertext concatenated in that order. You need to segment this array into its original components based on their predefined lengths.
Constants:
IV_LENGTH: 12 bytesSALT_LENGTH: 16 bytesGCM_TAG_LENGTH: 128 bits (which is 16 bytes for the authentication tag, handled internally by GCM).
Important Note: While the combined payload includes an embedded salt, the salt used for
deriveKeyFromPasswordmust be the one provided to you separately, not the one extracted from the combined ciphertext. The embedded salt is redundant for decryption but included for consistency in the combined payload format.Derive the Secret Key: Using the
passwordandsaltprovided to you, derive the AES secret key. This involves using a Key Derivation Function (KDF) likePBKDF2WithHmacSHA256.Constants for Key Derivation:
ITERATION_COUNT: 65536KEY_SIZE: 256 bits (for AES-256)PBKDF2_ALGORITHM:PBKDF2WithHmacSHA256(or equivalent identifier in your library)
Initialize the Cipher for Decryption: Instantiate an AES/GCM cipher object in decryption mode, providing the derived secret key and the extracted IV. The GCM tag length must also be specified.
Perform Decryption: Execute the decryption operation on the
actualCiphertextusing the initialized cipher. The result will be the plaintext in byte array form, which can then be converted to a string using UTF-8 encoding.Handle Potential Exceptions: Decryption can fail if the key, IV, or ciphertext is incorrect, or if the data has been tampered with (e.g., the GCM authentication tag does not verify). It is crucial to implement robust error handling to gracefully manage such scenarios. Your cryptographic library will typically throw an exception in case of authentication failure or invalid input.
4. Full Kotlin Decryption Example
Here is a complete Kotlin class demonstrating the decryption process, including error handling and the necessary constants.
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.nio.charset.StandardCharsets
import java.security.Security
import java.security.spec.KeySpec
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
// Custom exception for decryption failures
class EncryptionFailedException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
/**
* A utility class for decrypting data encrypted using AES/GCM/NoPadding with PBKDF2 key derivation.
*
* @param password The secret password used for key derivation.
* @param salt The salt string used for key derivation. This must be the same salt provided
* separately, not the one embedded in the encrypted payload.
*/
class PubSubDecryptor(
private val password: String,
private val salt: String
) {
// --- Cryptographic Constants ---
private companion object {
private const val ALGORITHM = "AES/GCM/NoPadding"
private const val GCM_TAG_LENGTH = 128 // 16 bytes authentication tag, number is in bits
private const val IV_LENGTH = 12 // 12 bytes IV for GCM
private const val KEY_SIZE = 256 // 256-bit AES key
private const val SALT_LENGTH = 16 // 16 bytes salt for PBKDF2 (this refers to the embedded salt length)
private const val ITERATION_COUNT = 65536 // PBKDF2 iteration count
private const val PBKDF2_WITH_HMAC_SHA256 = "PBKDF2WithHmacSHA256"
}
private val saltBytes: ByteArray // The salt provided to the constructor, converted to bytes
private val secretKey: SecretKey // The derived AES secret key
init {
// Ensure Bouncy Castle security provider is added.
// This should ideally be done once at application startup.
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(BouncyCastleProvider())
}
// Convert the provided salt string to bytes
saltBytes = this.salt.toByteArray(StandardCharsets.UTF_8)
// Derive the secret key from the password and salt
secretKey = deriveKeyFromPassword(password, saltBytes)
}
/**
* Decrypts a Base64-encoded ciphertext string.
* The input ciphertext is expected to be a concatenation of IV, embedded Salt, and actual Ciphertext.
*
* @param cipherText The Base64-encoded string to decrypt.
* @return The decrypted plaintext string.
* @throws EncryptionFailedException if the decryption process fails (e.g., due to incorrect key, IV, or tampered data).
*/
fun decrypt(cipherText: String): String {
// 1. Base64 decode the received ciphertext
val decodedCipherText: ByteArray = Base64.getDecoder().decode(cipherText)
// 2. Extract IV and actual ciphertext from the decoded payload
// The IV is the first IV_LENGTH bytes
val iv = ByteArray(IV_LENGTH)
System.arraycopy(decodedCipherText, 0, iv, 0, IV_LENGTH)
// The actual ciphertext starts after the IV and the embedded SALT_LENGTH bytes
// It includes the GCM authentication tag appended by the cipher during encryption.
val actualCipherText = ByteArray(decodedCipherText.size - IV_LENGTH - SALT_LENGTH)
System.arraycopy(decodedCipherText, 0, iv, 0, IV_LENGTH)
System.arraycopy(decodedCipherText, IV_LENGTH + SALT_LENGTH, actualCipherText, 0, actualCipherText.size)
// 3. Initialize the cipher for decryption
val cipher: Cipher = Cipher.getInstance(ALGORITHM, "BC") // "BC" specifies Bouncy Castle provider
val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH, iv) // GCM_TAG_LENGTH is in bits
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
// 4. Perform decryption and handle potential exceptions
try {
val decryptedText: ByteArray = cipher.doFinal(actualCipherText)
return String(decryptedText, StandardCharsets.UTF_8)
} catch (e: Exception) {
// Implement Error handling
System.err.println("Failed to decrypt the text: ${e.message}")
throw EncryptionFailedException("Failed to decrypt the text: ${e.message}", e)
}
}
/**
* Derives an AES secret key from a password and salt using PBKDF2WithHmacSHA256.
*
* @param password The password string.
* @param salt The salt byte array.
* @return The derived SecretKey for AES.
*/
private fun deriveKeyFromPassword(password: String, salt: ByteArray): SecretKey {
// Define the key derivation specification
val spec: KeySpec = PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_SIZE)
// Get a SecretKeyFactory instance for PBKDF2WithHmacSHA256
val factory: SecretKeyFactory = SecretKeyFactory.getInstance(PBKDF2_WITH_HMAC_SHA256)
// Generate the secret key bytes
val keyBytes: ByteArray = factory.generateSecret(spec).encoded
// Create an AES SecretKey from the derived bytes
return SecretKeySpec(keyBytes, "AES")
}
}
6. Data Contract and Handshake
The specific fields within a data payload that are deemed sensitive and subject to encryption will be clearly defined and communicated during the data contract or handshake agreement phase. This ensures transparency and allows both parties to understand which data elements require decryption. Any changes to the sensitive data schema will be communicated proactively.
7. Support
Should you encounter any issues or require further clarification during the implementation of the decryption process, please do not hesitate to reach out to your designated technical contact.