Encryption & Security
All API requests/responses must be encrypted using a hybrid encryption model.
UAT Environment
Base URL: https://api.vaultspay.dev
In UAT, you can optionally disable encryption by adding a header:
x-enable-encryption: falseProduction Environment
Base URL: https://rest.vaultspay.com
- Encryption is mandatory
- Use the RSA public key shared with you for encryption
Encryption Flow
- Generate AES key + IV on the client side
- Encrypt payload with AES key
- Encrypt AES key with RSA public key
- Send to backend:
- AES-encrypted data
- RSA-encrypted AES key
Response Decryption
- Use the same AES key used in the request to decrypt the backend response.
Required Headers
| Header | Value Description |
|---|---|
| x-session-nonce | IV used in AES encryption |
| x-auth-signature | AES key encrypted with RSA public key |
| x-device-os | ’SAAS’ |
| x-device-id | SaaS app ID (e.g., 17) |
Sample Java Code for Encryption/Decryption
public Map<String, String> encrypt(String plainText) {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256); // 256-bit AES
// Step 2: Generate AES Key
SecretKey aesKey = keyGen.generateKey();
// Step 3: Encode key in Base64
byte[] iv = new byte[12]; // 12 bytes = 96 bits (recommended for GCM)
new SecureRandom().nextBytes(iv);
String encrypt = encrypt(plainText, aesKey.getEncoded(), iv);
PublicKey publicKey = publicKeyFromBase64("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAne7/zCQBaojWUqVOYrCyzVMubq2cBJhepEKPri+qW6uh1HGCp7XZFXbl2T6XG0XJa4QFFjaQAo+BMAkCUo2JuL5cxyR7CsmaZgdeqtExteQ9rc92G0iMsI4qfSB9Hr3bqrmXqUEIMZJ7tboz51hyW1Mq4m1Y4bzlGwFT0WNyBbUMU/mMbSRd/nU7Py9qoGJJVJRwdNq7E+s8FPMXeWdMQw49yR7IGWayGJ6Fqwy6hnpZesPHAqKmiRCk5DRPMYa6RVk1k/B1PJX6xt/87L9Me+lBM/E5A30qg7f+oCbGZqVHeWBFJjZ6kpr5o9UtqzgdsHMGNln3wZw2C74klrVTBwIDAQAB");
String base64Iv = Base64.getEncoder().encodeToString(iv);
Map<String, String> result = new HashMap<>();
result.put("encryptedData", encrypt);
result.put("iv", base64Iv);
result.put("encryptedKey", encryptRsa(aesKey.getEncoded(), publicKey));
return result;
}
public String encrypt(String data, byte[] key, byte[] iv) throws Exception {
// Ensure the key is the correct length (32 bytes for AES-256)
byte[] truncatedKey = Arrays.copyOf(key, 32);
Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING);
SecretKey secretKey = new SecretKeySpec(truncatedKey, "AES");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
public String encryptRsa(byte[] aesKey, PublicKey publicKey) throws Exception {
// Step 1: Create cipher instance with matching padding
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING");
// Step 2: Configure OAEP params
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT
);
// Step 3: Initialize cipher in encrypt mode
cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
// Step 4: Encrypt the AES key
byte[] encryptedKey = cipher.doFinal(aesKey);
// Step 5: Return as Base64 string
return Base64.getEncoder().encodeToString(encryptedKey);
}
public String decrypt(String encryptedText) {
try {
// Decrypt the AES key using the server's RSA private key
byte[] encryptedAesKey = Base64.getDecoder().decode(aesKey);
byte[] decryptedAesKey = decryptRsa(encryptedAesKey, public_rsa_key);
byte[] iv = Base64.getDecoder().decode(iv);
return decrypt(encryptedText,
decryptedAesKey, iv
);
} catch (Exception ecx) {
log.info("Error in DecryptRequest::decrypt() ", ecx);
throw new SysException(ErrorCode.DECRYPTION_FAILED);
}
}
public byte[] decryptRsa(byte[] encryptedKey, PrivateKey privateKey) throws Exception {
// Use the same padding scheme as the client
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING");
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT
);
cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
return cipher.doFinal(encryptedKey);
}
public String decrypt(String encryptedData, byte[] key, byte[] iv) throws Exception {
// Ensure the key is the correct length (32 bytes for AES-256)
byte[] truncatedKey = Arrays.copyOf(key, 32);
Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING);
SecretKey secretKey = new SecretKeySpec(truncatedKey, "AES");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decrypted, StandardCharsets.UTF_8);
}Sample Python Code for Encryption/Decryption
def encrypt(plain_text: str, rsa_public_key):
# Step 1: Generate AES key (256-bit)
aes_key = os.urandom(32) # 256-bit AES key
# Step 2: Generate IV
iv = os.urandom(12) # 12 bytes = 96 bits for GCM
# Step 3: Encrypt data with AES GCM
encrypted_data = encrypt_aes_gcm(plain_text, aes_key, iv)
# Step 4: Encrypt AES key with RSA public key
public_key_b64 = (
rsa_public_key
)
public_key = load_rsa_public_key(public_key_b64)
encrypted_key = encrypt_rsa(aes_key, public_key)
# Step 5: Return results
return {
"encryptedData": base64.b64encode(encrypted_data).decode("utf-8"),
"iv": base64.b64encode(iv).decode("utf-8"),
"encryptedKey": base64.b64encode(encrypted_key).decode("utf-8"),
"rawAesKey": base64.b64encode(aes_key).decode("utf-8")
}
def encrypt_aes_gcm(data: str, key: bytes, iv: bytes) -> bytes:
encryptor = Cipher(
algorithms.AES(key),
modes.GCM(iv),
backend=default_backend()
).encryptor()
ciphertext = encryptor.update(data.encode("utf-8")) + encryptor.finalize()
return ciphertext + encryptor.tag # Append GCM auth tag
def encrypt_rsa(aes_key: bytes, public_key) -> bytes:
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
)
)
return encrypted_key
def load_rsa_public_key(b64_key: str):
der = base64.b64decode(b64_key)
return serialization.load_der_public_key(der, backend=default_backend())
def decrypt_aes_gcm(encrypted_data_b64: str, key: bytes, iv: bytes) -> str:
# Truncate or pad key to 32 bytes (AES-256)
truncated_key = key[:32]
cipher = AES.new(truncated_key, AES.MODE_GCM, nonce=iv)
decrypted = cipher.decrypt(base64.b64decode(encrypted_data_b64))
return decrypted.decode('utf-8')
def decrypt_rsa(encrypted_key_b64: str, private_key_pem: str) -> bytes:
encrypted_key = base64.b64decode(encrypted_key_b64)
private_key = serialization.load_pem_private_key(
private_key_pem.encode(),
password=None,
backend=default_backend()
)
decrypted_key = private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return decrypted_key
GCM_TAG_LENGTH = 16 # in bytes (128 bits)
def decrypt(encrypted_data_b64: str, iv_b64: str, aes_key: bytes) -> str:
try:
# Step 1: Decode Base64 values
iv = base64.b64decode(iv_b64)
encrypted_data = base64.b64decode(encrypted_data_b64)
# Step 2: Split encrypted_data into actual ciphertext and tag
ciphertext = encrypted_data[:-GCM_TAG_LENGTH]
tag = encrypted_data[-GCM_TAG_LENGTH:]
# Step 3: Create AES-GCM decryptor
cipher = Cipher(
algorithms.AES(aes_key),
modes.GCM(iv, tag),
backend=default_backend()
)
decryptor = cipher.decryptor()
# Step 4: Decrypt
decrypted_bytes = decryptor.update(ciphertext) + decryptor.finalize()
return decrypted_bytes.decode('utf-8')
except Exception as e:
print("Decryption failed:", e)
raise