Introduction

If you want to secure the data transmitted both ways, encryption can be enabled. This will affect your API requests and responses as well as webhooks received. We are using AES-256-GCM, a symmetric encryption algorithm that combines Advanced Encryption Standard with a 256-bit key length and Galois/Counter Mode (GCM). Encryption can be enabled for your team by contacting us.

API Format

All API requests and responses follow a standardized format with encryption versioning for future compatibility.

Content-Type

Content-Type: application/json+encrypted It is important to specify the correct content type when encryption is enabled.

Request and Response format

{
    "encrypted_payload": "base64_encoded_encrypted_data", 
    "encryption_version": "1.0"
}
The encryption_payload field holds the actual data, which, once decrypted, is identical to the payload of an unencrypted call. The encryption_version field allows for future cryptographic upgrades while maintaining backward compatibility.

Implementation example

encryption.py
import os
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import requests
import json

api_endpoint = "https://live.deck.co/api/v1/jobs/submit"

# Your API keys
client_id = "xxx"
secret = "xxx"

# Your encryption key
encryption_key = "xxx"

# Prepare your payload
payload = {
  "job_code": "EnsureConnection",
  "input": {
    "source_guid": "xxx",
    "username": "xxx",
    "password": "xxx"
  }
}

class AESEncryptionHelper:
    KEY_SIZE = 32  # 256 bits / 8
    NONCE_SIZE = 12  # 96 bits / 8
    TAG_SIZE = 16  # 128 bits / 8

    @staticmethod
    def generate_key():
        """
        Generates a new AES-256 key suitable for use with AES-GCM.
        
        Note: This is for testing and demonstration purposes only.
        Valid encryption keys for production use will be provided by Deck.
        """
        key = os.urandom(AESEncryptionHelper.KEY_SIZE)
        return base64.b64encode(key).decode('utf-8')

    @staticmethod
    def encrypt(plaintext, base64_key):
        """Encrypts a plaintext string using AES-256-GCM."""
        if not plaintext:
            raise ValueError("Plaintext cannot be empty")
        if not base64_key:
            raise ValueError("Key cannot be empty")

        key = base64.b64decode(base64_key)
        if len(key) != AESEncryptionHelper.KEY_SIZE:
            raise ValueError(f"Key must be {AESEncryptionHelper.KEY_SIZE} bytes")

        plaintext_bytes = plaintext.encode('utf-8')
        
        # Generate nonce
        nonce = os.urandom(AESEncryptionHelper.NONCE_SIZE)
        
        # Encrypt
        aesgcm = AESGCM(key)
        ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
        
        # Extract tag (last 16 bytes) and actual ciphertext
        actual_ciphertext = ciphertext[:-AESEncryptionHelper.TAG_SIZE]
        tag = ciphertext[-AESEncryptionHelper.TAG_SIZE:]
        
        # Combine: nonce + ciphertext + tag
        result = nonce + actual_ciphertext + tag
        
        return base64.b64encode(result).decode('utf-8')

    @staticmethod
    def decrypt(encrypted_data, base64_key):
        """Decrypts data that was encrypted using AES-256-GCM."""
        if not encrypted_data:
            raise ValueError("Encrypted data cannot be empty")
        if not base64_key:
            raise ValueError("Key cannot be empty")

        key = base64.b64decode(base64_key)
        if len(key) != AESEncryptionHelper.KEY_SIZE:
            raise ValueError(f"Key must be {AESEncryptionHelper.KEY_SIZE} bytes")

        data = base64.b64decode(encrypted_data)
        if len(data) < AESEncryptionHelper.NONCE_SIZE + AESEncryptionHelper.TAG_SIZE:
            raise ValueError("Invalid encrypted data format")

        # Extract components
        nonce = data[:AESEncryptionHelper.NONCE_SIZE]
        ciphertext = data[AESEncryptionHelper.NONCE_SIZE:-AESEncryptionHelper.TAG_SIZE]
        tag = data[-AESEncryptionHelper.TAG_SIZE:]
        
        # Decrypt
        aesgcm = AESGCM(key)
        plaintext = aesgcm.decrypt(nonce, ciphertext + tag, None)
        
        return plaintext.decode('utf-8')

# Initialize encryption helper
helper = AESEncryptionHelper()

# Convert to JSON and encrypt
json_payload = json.dumps(payload)
encrypted_payload = helper.encrypt(json_payload, encryption_key)

# Send to API using standardized format

payload = {
    "encrypted_payload": encrypted_payload,
    "encryption_version": "1.0"
}
print("Request Body:")
print(json.dumps(payload, indent=4))

response = requests.post(
    api_endpoint,
    headers={
        "Content-Type": "application/json+encrypted",
        "x-deck-client-id": client_id,
        "x-deck-secret": secret
    },
    json=payload
)

# Decrypt the response
if response.status_code == 200 or response.status_code == 202:
    response_data = response.json()
    print("\r\nResponse Body:")
    print(response_data)
    encrypted_response = response_data["encrypted_payload"]
    
    decrypted_response = helper.decrypt(encrypted_response, encryption_key)
    result = json.loads(decrypted_response)
    print("\r\nDecrypted response Payload:")
    print(result)
else:
    print(response.status_code)
Upon successful execution using your keys, the program will produce an output similar to this:
Request Body:
{
    "encrypted_payload": "b3Vcxxgp1/Vj74duDr3oQEmaAEqkcKDDLNiO8Pj55voKp4/X+d8sSZImd8awWTP/5+HUslkMelqvl2WcjPtbv5qpHfIPbt3IxIZoH+S8ictU0LOtDhRBXP4mJM8CptcH7roDx2G8F28wyM1FCY/ZUO5TLJfsk5XHY7vzq/quqU2SNMQrSCNd8y7IHtyX/65BsaEuZixdXdzWa5KfW7Cr+GAVXONEo6+vgXNTZlZ8NnOv+lLBbCuyaiEaPsGAm9NARJHxfKc=",
    "encryption_version": "1.0"
}

Response Body:
{'encrypted_payload': 'rpcnQBAb0k+6ctcXyRgfk06WhCyHTxv+xNMKjCIdiue9U5b97AfM2a+OIJRkpQ2kPqh4WIOQ81nL3wczRUwLa6+9/2gSIqNb42Pr+osmuKkdibkC49ip1GGz2HNH4dq76G4vsPx3Hs728Sc3QQ==', 'encryption_version': '1.0'}

Decrypted response Payload:
{'job_guid': 'acc5a357-db60-41b1-fc7b-08ddca2c5fc7', 'job_code': 'EnsureConnection'}
The webhooks can be decrypted exactly the same way.