Search code examples
javacryptographybouncycastlepost-quantum-cryptography

Implementing CRYSTALS-Kyber using BouncyCastle Java


Could anyone help lead me in the right direction to even just generate a keypair using Kyber? I've tried to search for BouncyCastle examples but I am unable to find any to go off.

I'm trying to benchmark some current algorithms such as AES/RSA, which I have working perfectly, but I am unable to make any progress on even getting started on Kyber. Any help would be greatly appreciated.

I've tried to search it on Google for examples, the developers have implementations C# and even Java without BouncyCastle on their website. They mention it is available on BouncyCastle but I cannot find any documentation for it even to help implement it.


Solution

  • I can provide a complete example for CHRYSTALS-KYBER using Bouncy Castle. It generates a keypair, calculates an encryption key and decryption key.

    There are three parameter specs available:

    KyberParameterSpec.kyber512
    KyberParameterSpec.kyber768
    KyberParameterSpec.kyber1024
    

    As the class was used in a larger Android project it may not work properly stand alone.

    I tested the algorithm using Bouncy Castle version 1.72.

    This is the full code:

    import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
    import org.bouncycastle.jcajce.spec.KEMExtractSpec;
    import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
    import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
    import org.bouncycastle.pqc.jcajce.spec.KyberParameterSpec;
    import org.bouncycastle.util.Arrays;
    
    import java.security.InvalidAlgorithmParameterException;
    import java.security.KeyFactory;
    import java.security.KeyPair;
    import java.security.KeyPairGenerator;
    import java.security.NoSuchAlgorithmException;
    import java.security.NoSuchProviderException;
    import java.security.PrivateKey;
    import java.security.PublicKey;
    import java.security.SecureRandom;
    import java.security.Security;
    import java.security.spec.InvalidKeySpecException;
    import java.security.spec.PKCS8EncodedKeySpec;
    import java.security.spec.X509EncodedKeySpec;
    
    import javax.crypto.KeyGenerator;
    
    public class PqcChrystalsKyberKem {
    
        public static void main(String[] args) {
            // Security.addProvider(new BouncyCastleProvider());
            // we do need the regular Bouncy Castle file that includes the PQC provider
            // get Bouncy Castle here: https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on
            // tested with BC version 1.72
            if (Security.getProvider("BCPQC") == null) {
                Security.addProvider(new BouncyCastlePQCProvider());
            }
            String print = run(false);
            System.out.println(print);
        }
    
        public static String run(boolean truncateKeyOutput) {
            String out = "PQC Chrystals-Kyber KEM";
            out += "\n" + "\n************************************\n" +
                    "* # # SERIOUS SECURITY WARNING # # *\n" +
                    "* This program is a CONCEPT STUDY  *\n" +
                    "* for the algorithm                *\n" +
                    "* Chrystals-Kyber [key exchange    *\n" +
                    "* mechanism]                       *\n" +
                    "* The program is using an          *\n" +
                    "* parameter set that I cannot      *\n" +
                    "* check for the correctness of the *\n" +
                    "* output and other details         *\n" +
                    "*                                  *\n" +
                    "*    DO NOT USE THE PROGRAM IN     *\n" +
                    "*    ANY PRODUCTION ENVIRONMENT    *\n" +
                    "************************************";
    
            // as there are 3 parameter sets available the program runs all of them
            KyberParameterSpec[] kyberParameterSpecs = {
                    KyberParameterSpec.kyber512,
                    KyberParameterSpec.kyber768,
                    KyberParameterSpec.kyber1024
            };
    
            // statistics
            int nrOfSpecs = kyberParameterSpecs.length;
            String[] parameterSpecName = new String[nrOfSpecs];
            int[] privateKeyLength = new int[nrOfSpecs];
            int[] publicKeyLength = new int[nrOfSpecs];
            int[] encryptionKeyLength = new int[nrOfSpecs];
            int[] encapsulatedKeyLength = new int[nrOfSpecs];
            boolean[] encryptionKeysEquals = new boolean[nrOfSpecs];
    
            out += "\n\n****************************************\n";
            for (int i = 0; i < nrOfSpecs; i++) {
                // generation of the Chrystals-Kyber key pair
                KyberParameterSpec kyberParameterSpec = kyberParameterSpecs[i];
                String kyberParameterSpecName = kyberParameterSpec.getName();
                parameterSpecName[i] = kyberParameterSpecName;
                out += "\n" + "Chrystals-Kyber KEM with parameterset " + kyberParameterSpecName;
                KeyPair keyPair = generateChrystalsKyberKeyPair(kyberParameterSpec);
    
                // get private and public key
                PrivateKey privateKey = keyPair.getPrivate();
                PublicKey publicKey = keyPair.getPublic();
    
                // storing the key as byte array
                byte[] privateKeyByte = privateKey.getEncoded();
                byte[] publicKeyByte = publicKey.getEncoded();
                out += "\n" + "\ngenerated private key length: " + privateKeyByte.length;
                out += "\n" + "generated public key length:  " + publicKeyByte.length;
                privateKeyLength[i] = privateKeyByte.length;
                publicKeyLength[i] = publicKeyByte.length;
    
                // generate the keys from a byte array
                PrivateKey privateKeyLoad = getChrystalsKyberPrivateKeyFromEncoded(privateKeyByte);
                PublicKey publicKeyLoad = getChrystalsKyberPublicKeyFromEncoded(publicKeyByte);
    
                // generate the encryption key and the encapsulated key
                out += "\n" + "\nEncryption side: generate the encryption key and the encapsulated key";
                SecretKeyWithEncapsulation secretKeyWithEncapsulationSender = pqcGenerateChrystalsKyberEncryptionKey(publicKeyLoad);
                byte[] encryptionKey = secretKeyWithEncapsulationSender.getEncoded();
                out += "\n" + "encryption key length: " + encryptionKey.length
                        + " key: " + bytesToHex(secretKeyWithEncapsulationSender.getEncoded());
                byte[] encapsulatedKey = secretKeyWithEncapsulationSender.getEncapsulation();
                out += "\n" + "encapsulated key length: " + encapsulatedKey.length + " key: " + (truncateKeyOutput ?shortenString(bytesToHex(encapsulatedKey)):bytesToHex(encapsulatedKey));
    
                encryptionKeyLength[i] = encryptionKey.length;
                encapsulatedKeyLength[i] = encapsulatedKey.length;
    
                out += "\n" + "\nDecryption side: receive the encapsulated key and generate the decryption key";
                byte[] decryptionKey = pqcGenerateChrystalsKyberDecryptionKey(privateKeyLoad, encapsulatedKey);
                out += "\n" + "decryption key length: " + decryptionKey.length + " key: " + bytesToHex(decryptionKey);
                boolean keysAreEqual = Arrays.areEqual(encryptionKey, decryptionKey);
                out += "\n" + "decryption key is equal to encryption key: " + keysAreEqual;
                encryptionKeysEquals[i] = keysAreEqual;
                out += "\n\n****************************************\n";
            }
    
            out += "\n" + "Test results";
            out += "\n" + "parameter spec name  priKL   pubKL encKL capKL  keyE"   + "\n";
            for (int i = 0; i < nrOfSpecs; i++) {
                String out1 = String.format("%-20s%6d%8d%6d%6d%6b%n", parameterSpecName[i], privateKeyLength[i], publicKeyLength[i], encryptionKeyLength[i], encapsulatedKeyLength[i], encryptionKeysEquals[i]);
                out += out1;
            }
            out += "\n" + "Legend: priKL privateKey length, pubKL publicKey length, encKL encryption key length, "
                    + "capKL encapsulated key length" + "\n";
            out += "****************************************\n";
            return out;
        }
    
        private static String shortenString (String input) {
            if (input != null && input.length() > 32) {
                return input.substring(0, 32) + " ...";
            } else {
                return input;
            }
        }
    
        private static KeyPair generateChrystalsKyberKeyPair(KyberParameterSpec kyberParameterSpec) {
            try {
                KeyPairGenerator kpg = KeyPairGenerator.getInstance("KYBER", "BCPQC");
                kpg.initialize(kyberParameterSpec, new SecureRandom());
                KeyPair kp = kpg.generateKeyPair();
                return kp;
            } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static SecretKeyWithEncapsulation pqcGenerateChrystalsKyberEncryptionKey(PublicKey publicKey) {
            KeyGenerator keyGen = null;
            try {
                keyGen = KeyGenerator.getInstance("KYBER", "BCPQC");
                keyGen.init(new KEMGenerateSpec((PublicKey) publicKey, "AES"), new SecureRandom());
                SecretKeyWithEncapsulation secEnc1 = (SecretKeyWithEncapsulation) keyGen.generateKey();
                return secEnc1;
            } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static byte[] pqcGenerateChrystalsKyberDecryptionKey(PrivateKey privateKey, byte[] encapsulatedKey) {
            KeyGenerator keyGen = null;
            try {
                keyGen = KeyGenerator.getInstance("KYBER", "BCPQC");
                keyGen.init(new KEMExtractSpec((PrivateKey) privateKey, encapsulatedKey, "AES"), new SecureRandom());
                SecretKeyWithEncapsulation secEnc2 = (SecretKeyWithEncapsulation) keyGen.generateKey();
                return secEnc2.getEncoded();
            } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        private static PrivateKey getChrystalsKyberPrivateKeyFromEncoded(byte[] encodedKey) {
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(encodedKey);
            KeyFactory keyFactory = null;
            try {
                keyFactory = KeyFactory.getInstance("KYBER", "BCPQC");
                return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
            } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        private static PublicKey getChrystalsKyberPublicKeyFromEncoded(byte[] encodedKey) {
            X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(encodedKey);
            try {
                KeyFactory keyFactory = KeyFactory.getInstance("KYBER", "BCPQC");
                return keyFactory.generatePublic(x509EncodedKeySpec);
            } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        private static String bytesToHex(byte[] bytes) {
            StringBuffer result = new StringBuffer();
            for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
            return result.toString();
        }
    }