Search code examples
javaswiftcryptokit

Swift AES GCM encryptor and Java decryption - Padding issues


I have a swift function to encrypt a string.

When I give it "abc" it creates a ciphertext without padding.

When I give it "a" it creates a ciphertext with padding "==".

This behavior is understood. The problem is I need to decrypt the string in java.


Java code decryptes the ciphertext of "abc" fine, but not of "a".

It throws error "input byte array has incorrect ending byte at...".

Obviously it is not able to decrypt the padding bytes.


How do I solve this issue in java code? I tried instantiating Cipher with AES/GCM/PKCS5Padding but it says cannot find any provider supporting this padding


SWIFT ENCRYPTOR

static func encrypt() {
        
            let plain = "a" // another string "abc"
      
            static let secret = "my-xxx-bit-secret-my-secret-my-s"
            static let nonceString = "fv1nixTVoYpSvpdA"
            static let nonce = try! AES.GCM.Nonce(data: Data(base64Encoded: nonceString)!)
            static let symKey = SymmetricKey(data: secret.data(using: .utf8)!)

            let sealedBox = try! AES.GCM.seal(plain.data(using: .utf8)!, using: symKey, nonce: nonce)
            let ciphertext = sealedBox.ciphertext.base64EncodedString()
            let tag = sealedBox.tag
            print("ciphertext: .\(ciphertext).")
            print("tag: \(tag.base64EncodedString())")
                 

}

JAVA DECRYPTOR

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

public class StringDecryptor {
    public static void main(String[] args) throws Exception {
    
        String actualText1 = "abc";
        String cipherText1 = "UoRs"; 
        String tag1 = "7VhlWAPpKka0CkmpshyOjw==";
        decryptSimpleString(cipherText1, tag1);
        
        String actualText2 = "a";
        String cipherText2 = "Ug==";  
        String tag2 = "hkjeGS301OgQyGqdGDuHAA==";
        decryptSimpleString(cipherText2, tag2);
     }
    
    public static void decryptSimpleString(String cipherText, String tag) throws Exception {
        String secret = "my-xxx-bit-secret-my-secret-my-s";
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        
        String nonce = "fv1nixTVoYpSvpdA";
        byte[] nonceBytes = Base64.getDecoder().decode(nonce);
        
        byte[] tagBytes = Base64.getDecoder().decode(tag);
        
        String ciphterTextWithTag = cipherText + tag;
        byte[] ciphertextBytes = Base64.getDecoder().decode(ciphterTextWithTag); 

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(128, nonceBytes);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);

        byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
        String plaintext = new String(plaintextBytes, StandardCharsets.UTF_8);
        System.out.println("plain text was "+plaintext);
    }
    
    
}

Output of Java code

plain text was abc

Exception in thread "main" java.lang.IllegalArgumentException: Input byte array has incorrect ending byte at 4
    at java.base/java.util.Base64$Decoder.decode0(Base64.java:875)
    at java.base/java.util.Base64$Decoder.decode(Base64.java:566)
    at java.base/java.util.Base64$Decoder.decode(Base64.java:589)
    at com.mydomain.crypto.StringDecryptor.decryptSimpleString(StringDecryptor.java:34)
    at com.mydomain.crypto.StringDecryptor.main(StringDecryptor.java:21)

Solution

  • When in doubt, check the documentation.

    The documentation for Base64 starts out with:

    This class consists exclusively of static methods for obtaining encoders and decoders for the Base64 encoding scheme. The implementation of this class supports the following types of Base64 as specified in RFC 4648 and RFC 2045.

    If we follow that first link, we see the Internet standard for Base64 encoding (and some other encodings). Section 4 says:

    A 65-character subset of US-ASCII is used, enabling 6 bits to be represented per printable character. (The extra 65th character, "=", is used to signify a special processing function.)

    And a little lower down, it says:

    Padding at the end of the data is performed using the '=' character. Since all base 64 input is an integral number of octets, only the following cases can arise:

    1. The final quantum of encoding input is an integral multiple of 24 bits; here, the final unit of encoded output will be an integral multiple of 4 characters with no "=" padding.
    2. The final quantum of encoding input is exactly 8 bits; here, the final unit of encoded output will be two characters followed by two "=" padding characters.
    3. The final quantum of encoding input is exactly 16 bits; here, the final unit of encoded output will be three characters followed by one "=" padding character.

    Hopefully, this makes it clear that the = character may only appear at the end of Base64 encoded bytes.

    Your code has this:

    String cipherText2 = "Ug==";  
    String tag2 = "hkjeGS301OgQyGqdGDuHAA==";
    decryptSimpleString(cipherText2, tag2);
    

    which means the first argument to decryptSimpleString, cipherText, is "Ug==", and the second argument to decyptSimpleString, tag, is a longer sequence of Base64 encoded bytes.

    The decryptSimpleString method contains this:

    String ciphterTextWithTag = cipherText + tag;
    byte[] ciphertextBytes = Base64.getDecoder().decode(ciphterTextWithTag); 
    

    Oops. cipherText contains = characters, and since the Base64 specification clearly states that = can only appear at the end of Base64 encoded content, cipherText + tag is not a valid Base64 sequence.

    In summary, you cannot just concatenate two Base64 sequences and assume the result is itself a valid Base64 sequence.

    If you have two Base64 sequences and you want to create a single sequence of bytes from them, decode each of them separately:

    Base64.Decoder decoder = Base64.getDecoder();
    byte[] decodedCipherText = decoder.decode(cipherText);
    byte[] decodedTag = decoder.decode(tag);
    
    byte[] ciphertextBytes =
        new byte[decodedCipherText.length + decodedTag.length];
    ByteBuffer.wrap(ciphertextBytes).put(decodedCipherText).put(decodedTag);