juspay/hyperswitch · error · GcpKmsError

Failed UTF-8 decode of GCP KMS decrypted output

Error message

Failed UTF-8 decode of GCP KMS decrypted output

What it means

Thrown by GcpKmsClient::decrypt when the KMS decryption succeeded but the returned plaintext bytes are not valid UTF-8. The library's decrypt returns a String, so after String::from_utf8(response.plaintext) fails, this error is applied. It means the ciphertext decrypts fine but the original secret was not UTF-8 text (e.g. it was raw binary data encrypted by a different client).

Source

Thrown at crates/external_services/src/gcp_kms/core.rs:164

}

/// Errors that could occur during GCP KMS operations.
#[derive(Debug, thiserror::Error)]
pub enum GcpKmsError {
    /// An error occurred when base64 decoding the input data.
    #[error("Failed to base64 decode input data")]
    Base64DecodingFailed,

    /// An error occurred when GCP KMS decrypting the input data.
    #[error("Failed to GCP KMS decrypt input data")]
    DecryptionFailed,

    /// An error occurred when GCP KMS encrypting the input data.
    #[error("Failed to GCP KMS encrypt input data")]
    EncryptionFailed,

    /// An error occurred UTF-8 decoding the GCP KMS decrypted output.
    #[error("Failed UTF-8 decode of GCP KMS decrypted output")]
    Utf8DecodingFailed,

    /// An error occurred when creating the GCP KMS client.
    #[error("Failed to create GCP KMS client")]
    ClientCreationFailed,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validate_fails_when_project_id_is_empty() {
        let config = GcpKmsConfig {
            project_id: String::new(),
            location_id: "global".to_string(),
            key_ring_id: "key-ring".to_string(),
            key_id: "key".to_string(),

View on GitHub (pinned to 806ec7dcc0)

Solutions

  1. Confirm the original secret is actually UTF-8 text; if it's binary, this decrypt API is not suitable — use the raw google_cloud_kms client for those payloads
  2. If a DEK or random key was encrypted, switch to envelope encryption and keep the decrypted bytes as Vec<u8>, never passing them through decrypt()
  3. Re-encrypt the secret as UTF-8 (e.g. hex or base64-encode binary data before encrypting with this client)
  4. Check for corrupted/truncated ciphertext that happens to decode to garbage bytes

Example fix

// before: encrypting binary data, then decrypt() must return String
client.encrypt(&binary_dek).await?;
let dek = client.decrypt(&ct).await?; // Utf8DecodingFailed: DEK is not UTF-8

// after: base64-encode binary payloads before encrypting with this client
let b64 = base64::engine::general_purpose::STANDARD.encode(&binary_dek);
let ct = client.encrypt(b64).await?;
let dek_b64 = client.decrypt(&ct).await?;
Defensive patterns

Strategy: validation

Validate before calling

// If you control encryption, force text payloads so decrypt() can return String:
fn prepare_for_kms(plaintext: &[u8]) -> String {
    if std::str::from_utf8(plaintext).is_ok() {
        String::from_utf8_lossy(plaintext).into_owned()
    } else {
        use base64::Engine;
        base64::engine::general_purpose::STANDARD.encode(plaintext)
    }
}
let ct = client.encrypt(prepare_for_kms(payload)).await?;

Try / catch

match client.decrypt(ct).await {
    Ok(s) => s,
    Err(e) if matches!(e.current_context(), GcpKmsError::Utf8DecodingFailed) => {
        // decrypted bytes are binary; this API cannot return them — re-encrypt as
        // base64/hex text or use a raw KMS client for binary payloads
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling GcpKmsClient::decrypt where the ciphertext decrypts to binary (non-UTF-8) plaintext — e.g. the data was encrypted with gcloud/kms API directly or by another service using GcpKmsClient::encrypt on binary bytes, since encrypt accepts arbitrary bytes (data: impl AsRef<[u8]>) but decrypt assumes UTF-8 output. Raised at String::from_utf8 at crates/external_services/src/gcp_kms/core.rs:108-109.

Common situations: Encrypting binary secrets (DER keys, serialized protobufs, random DEKs) via encrypt() then decrypting with this client, whose API only returns String; interop with other systems that encrypt raw bytes under the same KMS key; secrets containing legacy encodings (latin-1) rather than UTF-8.

Related errors


AI-assisted analysis of juspay/hyperswitch@806ec7dcc0 (2026-08-28). Data as JSON: /api/errors/baef64c93ab7110f. Report an issue: GitHub.