Skip to content
Advertisement

Mixing tweetnacl.js with TweetNaclFast (java) for asymmetric encryption

Our project is using asymmetric encryption with nacl.box and ephemeral keys:

    encrypt(pubKey, msg) {
        if (typeof msg !== 'string') {
            msg = JSON.stringify(msg)
        }
        let ephemKeys = nacl.box.keyPair()
        let msgArr = nacl.util.decodeUTF8(msg)
        let nonce = nacl.randomBytes(nacl.box.nonceLength)
        p(`naclRsa.pubKey=${this.pubKey}`)
        let encrypted = nacl.box(
            msgArr,
            nonce,
            nacl.util.decodeBase64(pubKey),
            ephemKeys.secretKey
        )
        let nonce64 = nacl.util.encodeBase64(nonce)
        let pubKey64 = nacl.util.encodeBase64(ephemKeys.publicKey)
        let encrypted64 = nacl.util.encodeBase64(encrypted)
        return {nonce: nonce64, ephemPubKey: pubKey64, encrypted: encrypted64}
    }

We presently have node.js apps that then decrypt these messages. We would like the option to use jvm languages for some features. There does not seem to be the richness of established players for tweet-nacl on the jvm but it seems

and its recommended implementation

° tweetnacl-fast https://github.com/InstantWebP2P/tweetnacl-java/blob/master/src/main/java/com/iwebpp/crypto/TweetNaclFast.java

were a popular one.

It is unclear what the analog to the asymmetric encryption with ephemeral keys were in that library. Is it supported? Note that I would be open to either java or kotlin if this were not supported in tweetnacl-java.

Advertisement

Answer

tweetnacl-java is a port of tweetnacl-js. It is therefore to be expected that both provide the same functionality. At least for the posted method this is the case, which can be implemented on the Java side with TweetNaclFast as follows:

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import com.iwebpp.crypto.TweetNaclFast;
import com.iwebpp.crypto.TweetNaclFast.Box;
import com.iwebpp.crypto.TweetNaclFast.Box.KeyPair;

...

private static EncryptedData encrypt(byte[] pubKey, String msg) {
    KeyPair ephemKeys = Box.keyPair();
    byte[] msgArr = msg.getBytes(StandardCharsets.UTF_8);
    byte[] nonce = TweetNaclFast.randombytes(Box.nonceLength);
    
    Box box = new Box(pubKey, ephemKeys.getSecretKey());
    byte[] encrypted = box.box(msgArr, nonce);
    
    String nonce64 = Base64.getEncoder().encodeToString(nonce);
    String ephemPubKey64 = Base64.getEncoder().encodeToString(ephemKeys.getPublicKey());
    String encrypted64 = Base64.getEncoder().encodeToString(encrypted);
    return new EncryptedData(nonce64, ephemPubKey64, encrypted64);
}

...

class EncryptedData {
    public EncryptedData(String nonce, String ephemPubKey, String encrypted) {
        this.nonce = nonce;
        this.ephemPubKey = ephemPubKey;
        this.encrypted = encrypted;
    }
    public String nonce;
    public String ephemPubKey;
    public String encrypted;
}

In order to demonstrate that both sides are compatible, in the following a plaintext is encrypted on the Java side and decrypted on the JavaScript side:

  • First, a key pair is needed on the JavaScript side, whose public key (publicKeyJS) is passed to the Java side. The key pair on the JavaScript side can be generated as follows:

    let keysJS = nacl.box.keyPair();
    let secretKeyJS = keysJS.secretKey;
    let publicKeyJS = keysJS.publicKey;
    console.log("Secret key: " + nacl.util.encodeBase64(secretKeyJS));
    console.log("Public key: " + nacl.util.encodeBase64(publicKeyJS));
    

    with the following sample output:

    Secret key: YTxAFmYGm4yV2OP94E4pcD6LSsN4gcSBBAlU105l7hw= 
    Public key: BDXNKDHeq0vILm8oawAGAQtdIsgwethzBTBqmsWI+R8=
    
  • The encryption on the Java side is then using the encrypt method posted above (and publicKeyJS):

    byte[] publicKeyJS = Base64.getDecoder().decode("BDXNKDHeq0vILm8oawAGAQtdIsgwethzBTBqmsWI+R8=");
    EncryptedData encryptedFromJava = encrypt(publicKeyJS, "I've got a feeling we're not in Kansas anymore...");
    System.out.println("Nonce: " + encryptedFromJava.nonce);
    System.out.println("Ephemeral public key: " + encryptedFromJava.ephemPubKey);
    System.out.println("Ciphertext: " + encryptedFromJava.encrypted);
    

    with the following sample output:

    Nonce: FcdzXfYwSbI0nq2WXsLe9aAh94vXSoWd
    Ephemeral public key: Mde+9metwF1jIEij5rlZDHjAStR/pd4BN9p5JbZleSg=
    Ciphertext: hHo7caCxTU+hghcFZFv+djAkSlWKnC12xj82V2R/Iz9GdOMoTzjoCDcz9m/KbRN6i5dkYi3+Gf0YTtKlZQWFooo=
    
  • The decryption on the JS side gives the original plaintext (using secretKeyJS):

    let nonce = "FcdzXfYwSbI0nq2WXsLe9aAh94vXSoWd";
    let ephemPubKey = "Mde+9metwF1jIEij5rlZDHjAStR/pd4BN9p5JbZleSg=";
    let encrypted = "hHo7caCxTU+hghcFZFv+djAkSlWKnC12xj82V2R/Iz9GdOMoTzjoCDcz9m/KbRN6i5dkYi3+Gf0YTtKlZQWFooo=";
    let secretKeyJS = nacl.util.decodeBase64("YTxAFmYGm4yV2OP94E4pcD6LSsN4gcSBBAlU105l7hw=");
    let decryptedFromJS = decrypt(secretKeyJS, {nonce: nonce, ephemPubKey: ephemPubKey, encrypted: encrypted});
    console.log(nacl.util.encodeUTF8(decryptedFromJS)); // I've got a feeling we're not in Kansas anymore...
      
    function decrypt(secretKey, ciphertext){
        let decrypted = nacl.box.open(
            nacl.util.decodeBase64(ciphertext.encrypted),
            nacl.util.decodeBase64(ciphertext.nonce),
            nacl.util.decodeBase64(ciphertext.ephemPubKey),
            secretKey
        );
        return decrypted;
    }
    <script src="https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
    
       
Advertisement