Goodays End-to-End Encryption Guide

If you wish to secure user data flowing to Goodays, you can encrypt sensitive parameters (like cz_user, cz_x_, etc.) using AES-256-CBC.

This guide provides the technical specifications and code examples to implement this encryption correctly.

Technical Specifications

ParameterValueExample
AlgorithmAES-
ModeCBC (Cipher Block Chaining)-
Key Size256 bits (32 bytes)MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= (Base64)
IV Size128 bits (16 bytes)MDEyMzQ1Njc4OWFiY2RlZg== (Base64)
PaddingCustom (0x1C) - Do not use PKCS#7...data\x1c\x1c\x1c
Output EncodingBase64 string, then URL EncodedSww0ZtK5...

Non-Standard Padding

Please read this carefully before implementation.

Our system uses a custom padding character to complete AES blocks:

  • Character: \34 (Octal)
  • Hex Value: 0x1C
  • Decimal Value: 28

Most standard AES libraries (OpenSSL, Crypto, etc.) default to PKCS#7 padding.

You MUST disable default padding (PKCS#7) and implement the manual padding logic described below. Failure to do so will result in decryption errors on the Goodays side.

Encryption Workflow

  1. Prepare Data: Convert your string to UTF-8 bytes.
  2. Apply Padding: Calculate the number of missing bytes to reach a multiple of 16. Append the character 0x1C repeatedly.
  3. Encrypt: Encrypt the padded bytes using your Key and IV.
  4. Encode:
    1. Convert the encrypted binary result to a Base64 string.
    2. URL Encode the Base64 string.

Reference Implementation

The following keys are for testing purposes only. Production keys will be communicated separately.

  • Test Key (Base64): MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
    • (Decodes to: 0123456789abcdef0123456789abcdef)
  • Test IV (Base64): MDEyMzQ1Njc4OWFiY2RlZg==
    • (Decodes to: 0123456789abcdef)

Expected Output for Validation

Input String: Tatsuo|Kusakabe|[email protected]|0612345789|123456789

Encrypted Result (URL Encoded):

Sww0ZtK5thbCpUeYSDe63w0Jiiv%2FgkadCJZN0LXJvSe1234j3Yk8NqZSFSiDreotkn5qw66Yr3RImtcg%2B4eBzCuNhPPwtcsbs9huAupW0vqLxu%2B1b3q8XMP2hx

Code Examples

🚧

These examples are provided as a rough indication of how encryption should be set up on your side. We do not guarantee compatibility with your IT system.

In each code, please replace <KEY> and <IV> with your dedicated values.

<?php
define('CIPHER_METHOD', 'AES-256-CBC');
// Configuration
define('KEY_B64', '<KEY>');
define('IV_B64',  '<IV>');
define('PADDING_CHAR', "\x1C"); 

function validateBase64($str, $name) {
    // strict mode = true returns false on failure
    $decoded = base64_decode($str, true);
    if ($decoded === false) {
        echo "❌ CONFIG ERROR: The $name provided is not valid Base64.\n";
        exit(1);
    }
    return $decoded;
}

function encryptGoodays($text) {
    // Decode Keys with Validation
    $key = validateBase64(KEY_B64, "KEY");
    $iv  = validateBase64(IV_B64, "IV");
    
    // 1. Manual Padding
    $blockSize = 16;
    $pad = $blockSize - (strlen($text) % $blockSize);
    $padded_text = $text . str_repeat(PADDING_CHAR, $pad);
    
    // 2. Encrypt
		// Note: Use OPENSSL_ZERO_PADDING to prevent automatic PKCS#7 padding.
    $encrypted = openssl_encrypt(
        $padded_text, 
        CIPHER_METHOD, 
        $key, 
        OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, 
        $iv
    );
    
    // 3. Base64 -> URL Encode
    return urlencode(base64_encode($encrypted));
}

function decryptGoodays($text) {
    $key = validateBase64(KEY_B64, "KEY");
    $iv  = validateBase64(IV_B64, "IV");
    
    // 1. URL Decode -> Base64 Decode
    $encrypted_data = base64_decode(urldecode($text));
    
    // 2. Decrypt
    $decrypted_padded = openssl_decrypt(
        $encrypted_data, 
        CIPHER_METHOD, 
        $key, 
        OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, 
        $iv
    );
    
    // 3. Remove Padding
    return rtrim($decrypted_padded, PADDING_CHAR);
}

// Test
echo "\n--- TEST START ---\n";
$msg = "Tatsuo|Kusakabe|[email protected]|0612345789|123456789";
echo "Original:   " . $msg . "\n";

// Only run if keys are configured
if (KEY_B64 === '<KEY>') {
    echo "Status:     ⚠️ SKIPPED - Please configure KEY_B64 and IV_B64 before running.\n";
} else {
    $encrypted = encryptGoodays($msg);
    echo "Encrypted:  " . $encrypted . "\n";

    $decrypted = decryptGoodays($encrypted);
    echo "Decrypted:  " . $decrypted . "\n";

    if ($msg === $decrypted) {
        echo "Status:     ✅ SUCCESS - Decryption matches original\n";
    } else {
        echo "Status:     ❌ FAILED - Decryption mismatch\n";
    }
}
echo "--- TEST END ---\n";
?>
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.net.URLEncoder;
import java.net.URLDecoder;

public class GoodaysEncryption {

    // Configuration (Base64 Strings)
    private static final String KEY_B64 = "<KEY>";
    private static final String IV_B64  = "<IV>";
    
    // Custom Padding Character 0x1C
    private static final byte PADDING_VAL = 0x1C;

    private static byte[] decodeBase64(String val, String name) {
        try {
            return Base64.getDecoder().decode(val);
        } catch (IllegalArgumentException e) {
            System.err.println("❌ CONFIG ERROR: The " + name + " provided is not valid Base64.");
            System.exit(1);
            return null;
        }
    }

    public static String encrypt(String value) throws Exception {
        byte[] keyBytes = decodeBase64(KEY_B64, "KEY");
        byte[] ivBytes  = decodeBase64(IV_B64, "IV");

        // 1. Prepare Data and Calculate Padding
        byte[] inputBytes = value.getBytes(StandardCharsets.UTF_8);
        int blockSize = 16;
        int paddingLength = blockSize - (inputBytes.length % blockSize);
        int totalLength = inputBytes.length + paddingLength;

        // 2. Create Padded Array (Manual Padding)
        byte[] paddedBytes = Arrays.copyOf(inputBytes, totalLength);
        for (int i = inputBytes.length; i < totalLength; i++) {
            paddedBytes[i] = PADDING_VAL;
        }

        // 3. Encrypt (Use NoPadding to prevent automatic PKCS7)
      	// Note: Java defaults to PKCS5Padding. We use NoPadding and handle it manually.
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);

        byte[] encryptedBytes = cipher.doFinal(paddedBytes);

        // 4. Base64 Encode -> URL Encode
        String base64String = Base64.getEncoder().encodeToString(encryptedBytes);
        return URLEncoder.encode(base64String, StandardCharsets.UTF_8.toString());
    }

    public static String decrypt(String encryptedValue) throws Exception {
        byte[] keyBytes = decodeBase64(KEY_B64, "KEY");
        byte[] ivBytes  = decodeBase64(IV_B64, "IV");

        // 1. URL Decode -> Base64 Decode
        String urlDecoded = URLDecoder.decode(encryptedValue, StandardCharsets.UTF_8.toString());
        byte[] encryptedBytes = Base64.getDecoder().decode(urlDecoded);

        // 2. Decrypt
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

        byte[] decryptedPadded = cipher.doFinal(encryptedBytes);

        // 3. Remove Padding (Manual)
        int i = decryptedPadded.length - 1;
        while (i >= 0 && decryptedPadded[i] == PADDING_VAL) {
            i--;
        }
        byte[] decryptedBytes = Arrays.copyOf(decryptedPadded, i + 1);

        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) {
        try {
            if (KEY_B64.equals("<KEY>")) {
                throw new Exception("Please configure KEY_B64 and IV_B64 in the code.");
            }

            String msg = "Tatsuo|Kusakabe|[email protected]|0612345789|123456789";
            
            System.out.println("\n--- TEST START ---");
            System.out.println("Original:   " + msg);
            
            String encrypted = encrypt(msg);
            System.out.println("Encrypted:  " + encrypted);
            
            String decrypted = decrypt(encrypted);
            System.out.println("Decrypted:  " + decrypted);
            
            if (msg.equals(decrypted)) {
                System.out.println("Status:     ✅ SUCCESS - Decryption matches original");
            } else {
                System.out.println("Status:     ❌ FAILED - Decryption mismatch");
            }
            System.out.println("--- TEST END ---\n");
            
        } catch (Exception e) {
            System.out.println("Status:     ❌ ERROR - " + e.getMessage());
        }
    }
}
# Note: Requires pycryptodome library (pip install pycryptodome).

import base64
import urllib.parse
import binascii
import sys
from Crypto.Cipher import AES

# Configuration (Base64 Strings)
KEY_B64 = "<KEY>"
IV_B64  = "<IV>"

# Custom Padding Character (0x1C)
PADDING_CHAR = b'\x1c'
BLOCK_SIZE = 16

def get_bytes(value, name="value"):
    """Helper to ensure we handle bytes, not strings, with validation."""
    if isinstance(value, str):
        try:
            return base64.b64decode(value, validate=True)
        except binascii.Error as e:
            print(f"❌ CONFIG ERROR: The {name} provided is not a valid Base64 string.")
            print(f"   Details: {e}")
            sys.exit(1)
    return value

def add_padding(data_bytes):
    # Calculate padding length to reach multiple of 16
    padding_len = BLOCK_SIZE - (len(data_bytes) % BLOCK_SIZE)
    # Append the custom character 0x1C repeatedly
    return data_bytes + (PADDING_CHAR * padding_len)

def remove_padding(data_bytes):
    # Remove the specific 0x1C padding character from the end
    return data_bytes.rstrip(PADDING_CHAR)

def encrypt(text):
    key = get_bytes(KEY_B64, "KEY")
    iv  = get_bytes(IV_B64, "IV")
    
    # 1. Convert to bytes
    data_bytes = text.encode('utf-8')
    
    # 2. Add Custom Padding (CRITICAL STEP)
    padded_data = add_padding(data_bytes)
    
    # 3. Encrypt AES-CBC
    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypted_bytes = cipher.encrypt(padded_data)
    
    # 4. Base64 -> URL Encode
    return urllib.parse.quote_plus(base64.b64encode(encrypted_bytes).decode('utf-8'))

def decrypt(encrypted_url_string):
    key = get_bytes(KEY_B64, "KEY")
    iv  = get_bytes(IV_B64, "IV")
    
    # 1. URL Decode -> Base64 Decode
    try:
        encrypted_bytes = base64.b64decode(urllib.parse.unquote_plus(encrypted_url_string))
    except binascii.Error:
         raise ValueError("Input string is not valid Base64 data.")

    # 2. Decrypt
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_padded = cipher.decrypt(encrypted_bytes)
    
    # 3. Remove Padding -> Decode to String
    return remove_padding(decrypted_padded).decode('utf-8')

# Test
if __name__ == "__main__":
    msg = "Tatsuo|Kusakabe|[email protected]|0612345789|123456789"
    
    print("\n--- TEST START ---")
    print(f"Original:   {msg}")
    
    try:
        if KEY_B64 == "<KEY>":
             raise ValueError("Please configure KEY_B64 and IV_B64 in the script.")

        encrypted = encrypt(msg)
        print(f"Encrypted:  {encrypted}")
        
        decrypted = decrypt(encrypted)
        print(f"Decrypted:  {decrypted}")
        
        if msg == decrypted:
            print("Status:     ✅ SUCCESS - Decryption matches original")
        else:
            print("Status:     ❌ FAILED - Decryption mismatch")
    except Exception as e:
        print(f"Status:     ❌ ERROR - {e}")
        
    print("--- TEST END ---\n")