为什么此 Java NaCl 加密不适用于 GitHub Actions Secrets

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

我正在尝试编写一个 Java 应用程序,它在 GitHub 存储库中创建秘密以供 GitHub 操作使用。有许多可用的 SodiumLib 包装器,但它们通常包装本机 C 库。我一直在寻找一个纯粹的 Java 实现。

https://github.com/NeilMadden/salty-coffee 似乎是我需要的,而且库确实会创建加密字符串。下面的 Groovy 脚本采用密钥和输入值,并生成加密值:

@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

例如:

groovy encrypt.groovy 2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU= test

问题是生成的值在用于创建新值时被忽略 GitHub API secret。您尝试创建密钥,并且 HTTP 请求工作正常,但是当您在工作流中使用它时密钥为空。

从这个生成加密值的 Python 脚本创建的秘密工作正常,所以我知道我正在进行正确的 HTTP 调用并使用正确的密钥生成 GitHub 秘密:

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]))

我在 Java(或 Groovy)示例中做错了什么?

Python代码使用了sealed boxes,Java/Groovy代码没有,所以两者不兼容。

由于生成的密文不确定,因此无法进行直接比较。一个合理的测试是使用相同的代码来解密两个代码的密文。
以下Python代码使用贴出的代码进行加密,然后用补码进行解密。此代码稍后将用于测试Java代码:

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

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 似乎不支持密封盒(尽管它也是 Libsodium 库的包装器)来演示迁移。对于其他库(即使是纯 Java 库,如果有的话),这应该大致类似:

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);

如果用这段代码生成的密文作为Python代码中的密文,则可以成功解密,说明两种代码的加密功能完全相同。


编辑:

作为另一个库的替代品,salty-coffee 可以扩展以支持密封盒。

如果寄件人使用密封箱,基本上会出现以下情况:

  1. 首先,生成一个临时密钥对:ephemSKephemPK
  2. PK为接收者的public键。一个24字节的nonce是这样确定的:nonce = Blake2b(ephemPK || PK)
  3. 使用 CryptoBox 执行加密,使用 ephemSK 作为密钥,PK 作为 public 密钥和先前生成的 nonce.
  4. CryptoBoxreturns密文与16字节的拼接MAC。 ephemPK 被添加到密文前面。这3部分拼接起来就是封箱的结果。

salty-coffee 提供除 Blake2b 之外的所有 Libsodium 功能。为此,您可以使用例如充气城堡.

一个可能的实现是:

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); 

与:

// 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;
}

用上面的Python代码可以成功解密用该代码创建的密文,证明兼容性。