Home > Blockchain >  Why does this Java NaCl encryption not work with GitHub Actions Secrets
Why does this Java NaCl encryption not work with GitHub Actions Secrets

Time:03-22

I'm attempting to write a Java app that creates secrets in a GitHub repo to be consumed by GitHub Actions. There are many SodiumLib wrappers available, but they generally wrap up the native C library. I was looking for a pure Java implementation.

https://github.com/NeilMadden/salty-coffee appears to be what I need, and it does appear that the library will create encrypted string. The Groovy script below takes a key and input value, and generates an encrypted value:

@Grab(group='software.pando.crypto', module='salty-coffee', version='1.0.4')
@Grab(group='org.apache.commons', module='commons-lang3', version='3.12.0')
@Grab(group='commons-codec', module='commons-codec', version='1.15')

import software.pando.crypto.nacl.*
import org.apache.commons.lang3.*
import java.util.*
import org.apache.commons.codec.binary.Base64
import java.security.*
import java.nio.charset.*

def base64 = new Base64(3)

def key = base64.decode(args[0])
def value = StringUtils.defaultIfEmpty(args[1], "")

println "Encrypting "   value

def keyPair = CryptoBox.keyPair();
def githubPublicKey = CryptoBox.publicKey(key)

def box = CryptoBox.encrypt(keyPair.getPrivate(), githubPublicKey, value)
    
def out = new ByteArrayOutputStream()
box.writeTo(out);
out.flush();

def encryptedValue = new String(base64.encode(out.toByteArray()))

println encryptedValue

For example:

groovy encrypt.groovy 2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r dB7TJyvvcCU= test

The problem is that the generated value is ignored when it is used to create a new GitHub API secret. You attempt to create the secret, and the HTTP requests works ok, but the secret is empty when you use it in a workflow.

Secrets created from this Python script that generates encrypted values works fine however, so I know I am making the correct HTTP calls and using the correct keys to generate a GitHub secret:

from base64 import b64encode
from nacl import encoding, public
import sys

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")
  
print(encrypt(sys.argv[1], sys.argv[2]))

What am I doing wrong with the Java (or Groovy) example?

CodePudding user response:

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 salted_coffy (at least I haven't found a way). Therefore I use lazysodium to demonstrate the migration, but this should be similar for other libraries:

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.

  • Related