The Python code uses sealed boxes, the Java/Groovy code does not, so the two are not compatible.
Since the generated ciphertexts are not deterministic, a direct comparison is not possible. A reasonable test is to decrypt the ciphertexts of both codes using the same code.
The following Python code uses the posted code for encryption and then performs decryption with the supplemented code. This code will be used to test the Java code later:
from base64 import b64encode, b64decode
from nacl import encoding, public
def encrypt(public_key: str, secret_value: str) -> str:
"""Encrypt a Unicode string using the public key."""
public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder())
sealed_box = public.SealedBox(public_key)
encrypted = sealed_box.encrypt(secret_value.encode("utf-8"))
return b64encode(encrypted).decode("utf-8")
pkB64 = 'xBC9lTyWdE/6EObv5NjryMbIvrviOzzPA+5XyM0QcHE='
skB64 = '0b9867Pq6sEdnxYM1ZscOhiMpruKn1Xg3xxB+wUF5eI='
# Encryption
encrypted = encrypt(pkB64, 'test')
# Decryption
secret_key = public.PrivateKey(skB64.encode("utf-8"), encoding.Base64Encoder())
unseal_box = public.SealedBox(secret_key)
plaintext = unseal_box.decrypt(b64decode(encrypted))
print(plaintext.decode('utf-8')) # test
Sealed boxes don't seem to be supported by salty-coffee (at least I haven't found a way). Therefore, and because I don't know of any pure Java library that supports sealed boxes, I use lazysodium (which is also a wrapper over the Libsodium library, though) to demonstrate the migration. For other libraries (even pure Java libraries, if there are any), this should be largely analogous:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HexFormat;
import com.goterl.lazysodium.LazySodiumJava;
import com.goterl.lazysodium.SodiumJava;
import com.goterl.lazysodium.utils.Key;
import com.goterl.lazysodium.utils.KeyPair;
....
SodiumJava sodium = new SodiumJava();
LazySodiumJava lazySodium = new LazySodiumJava(sodium, StandardCharsets.UTF_8);
Key secretKey = Key.fromBase64String("0b9867Pq6sEdnxYM1ZscOhiMpruKn1Xg3xxB+wUF5eI=");
Key publicKey = Key.fromBase64String("xBC9lTyWdE/6EObv5NjryMbIvrviOzzPA+5XyM0QcHE=");
// Encryption
KeyPair keyPair = new KeyPair(publicKey, secretKey);
String ciphertext = lazySodium.cryptoBoxSealEasy("test", publicKey);
System.out.println(Base64.getEncoder().encodeToString(HexFormat.of().parseHex(ciphertext)));
// Decryption
String decrypted = lazySodium.cryptoBoxSealOpenEasy(ciphertext, keyPair);
System.out.println(decrypted);
If a ciphertext generated with this code is used as ciphertext in the Python code, it can be successfully decrypted, which shows that the encryption of both codes is functionally identical.
Edit:
As an alternative to another library, salty-coffee can be extended to support sealed boxes.
If the sender uses a sealed box, basically the following happens:
- first, an ephemeral key pair is generated:
ephemSK, ephemPK.
- let
PK be the public key of the receiver. A 24 bytes nonce is determined as follows: nonce = Blake2b(ephemPK || PK)
- an encryption is performed with CryptoBox using
ephemSK as secret key, PK as public key and the previously generated nonce.
CryptoBox returns the concatenation of ciphertext and the 16 bytes MAC. ephemPK is prepended to the ciphertext. The concatenation of these 3 parts is the result of the sealed box.
salty-coffee provides all Libsodium functionalities needed for the implementation except Blake2b. For this you can use e.g. Bouncycastle.
A possible implmentation is:
import software.pando.crypto.nacl.*;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.util.Base64;
import org.bouncycastle.crypto.digests.Blake2bDigest;
...
byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
// Sender's secret key SK, receiver's public key PK
byte[] SK = Base64.getDecoder().decode("0b9867Pq6sEdnxYM1ZscOhiMpruKn1Xg3xxB+wUF5eI=");
byte[] PK = Base64.getDecoder().decode("xBC9lTyWdE/6EObv5NjryMbIvrviOzzPA+5XyM0QcHE=");
// Create an ephemeral keypair: ephemSK, ephemPK
KeyPair ephemKeyPair = CryptoBox.keyPair();
byte[] ephemSK_pkcs8 = ephemKeyPair.getPrivate().getEncoded();
byte[] ephemPK_x509 = ephemKeyPair.getPublic().getEncoded();
byte[] ephemSK = getRawKey(ephemSK_pkcs8);
byte[] ephemPK = getRawKey(ephemPK_x509);
// Create the nonce = Blake2b(ephemeralPK || PK))
byte[] noncematerial = new byte[64];
System.arraycopy(ephemPK, 0, noncematerial, 0, ephemPK.length);
System.arraycopy(PK, 0, noncematerial, ephemPK.length, PK.length);
byte[] nonce = new byte[24];
Blake2bDigest dig = new Blake2bDigest(null, nonce.length, null, null);
dig.update(noncematerial, 0, noncematerial.length);
dig.doFinal(nonce, 0);
// Encrypt with CryptoBox using ephemSK, PK and the nonce
CryptoBox cryptobox = CryptoBox.encrypt(CryptoBox.privateKey(ephemSK), CryptoBox.publicKey(PK), nonce, plaintext);
byte[] ciphertextMAC = cryptobox.getCiphertextWithTag();
// Prepend ephemPK
byte[] secretBoxSealed = new byte[ephemPK.length + ciphertextMAC.length];
System.arraycopy(ephemPK, 0, secretBoxSealed, 0, ephemPK.length);
System.arraycopy(ciphertextMAC, 0, secretBoxSealed, ephemPK.length, ciphertextMAC.length);
String secretBoxSealedB64 = Base64.getEncoder().encodeToString(secretBoxSealed);
System.out.println(secretBoxSealedB64);
with:
// The raw keys are the last 32 bytes in PKCS#8 and X.509 formatted keys respectively.
private static byte[] getRawKey(byte[] key) {
byte[] result = new byte[32];
System.arraycopy(key, key.length - result.length, result, 0, result.length);
return result;
}
A ciphertext created with this code can be successfully decrypted by the Python code above, proving the compatability.