Si alguna vez has implementado firmas digitales en Java usando directamente la JCA (Java Cryptography Architecture), conoces la sensación: lo que debería ser una operación sencilla se convierte en una ceremonia de factory methods, manejo de excepciones y encantamientos arcanos. Déjame mostrarte a qué me refiero.
El camino JCA
Aquí tienes un ciclo completo de firma y verificación usando solo la biblioteca estándar:
import java.security.*;
public class JcaSignatureExample {
public static void main(String[] args) throws Exception {
// Generar un par de claves
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
byte[] message = "Transferir 500€ a Alice".getBytes("UTF-8");
// Firmar
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(keyPair.getPrivate());
signer.update(message);
byte[] signatureBytes = signer.sign();
// Verificar
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message);
boolean isValid = verifier.verify(signatureBytes);
System.out.println("Válida: " + isValid);
}
}
Son 16 líneas de lógica (sin contar imports y boilerplate de la clase)
solo para firmar un mensaje y verificar la firma. Y este es el camino
feliz — sin carga de keystore, sin codificación, sin manejo de errores.
Cada llamada puede lanzar una excepción checked. La cadena del algoritmo
está duplicada. El objeto Signature es mutable y con estado: haces
init, luego update, luego llamas a sign o verify. Sáltate un paso
y obtienes una críptica SignatureException.
Ahora imagina cargar claves desde un keystore PKCS#12. Así es como se ve:
import java.io.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.Base64;
public class JcaKeystoreSignature {
public static void main(String[] args)
throws KeyStoreException, IOException, CertificateException,
NoSuchAlgorithmException, UnrecoverableKeyException,
InvalidKeyException, SignatureException {
// Cargar el keystore
KeyStore keystore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("keystore.p12")) {
keystore.load(is, "changeit".toCharArray());
}
// Extraer claves
PrivateKey privateKey = (PrivateKey)
keystore.getKey("alice", "changeit".toCharArray());
PublicKey publicKey =
keystore.getCertificate("alice").getPublicKey();
byte[] message = "Transferir 500€ a Alice".getBytes("UTF-8");
// Firmar
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(message);
byte[] signatureBytes = signer.sign();
// Codificar para transporte
String encoded = Base64.getEncoder().encodeToString(signatureBytes);
// Decodificar y verificar
byte[] decoded = Base64.getDecoder().decode(encoded);
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(message);
boolean isValid = verifier.verify(decoded);
System.out.println("Válida: " + isValid);
}
}
Treinta líneas de lógica. Siete excepciones checked en la cláusula
throws. Un cast a PrivateKey. Codificación Base64 manual. Y sin
embargo, todo lo que estamos haciendo es: cargar claves, firmar un mensaje,
verificar la firma.
El camino Bruce
Aquí está la misma operación con Bruce:
import static com.mirkocaserta.bruce.Bruce.*;
import static com.mirkocaserta.bruce.Keystores.*;
var keys = keyPair("RSA", 2048);
var signer = signerBuilder()
.key(keys.getPrivate())
.algorithm("SHA256withRSA")
.build();
var verifier = verifierBuilder()
.key(keys.getPublic())
.algorithm("SHA256withRSA")
.build();
var message = Bytes.from("Transferir 500€ a Alice");
var signature = signer.sign(message);
var isValid = verifier.verify(message, signature);
Eso es todo. Sin excepciones checked contaminando tu API. Sin estado
mutable que gestionar. Sin la danza manual de init/update/sign.
Signer y Verifier son interfaces funcionales — inmutables, thread-safe
y componibles.
Y con un keystore:
var keystore = keystore("classpath:keystore.p12", "changeit".toCharArray(), "PKCS12");
var privateKey = privateKey(keystore, "alice", "changeit".toCharArray());
var publicKey = publicKey(keystore, "alice");
var signer = signerBuilder()
.key(privateKey)
.algorithm("SHA256withRSA")
.build();
var verifier = verifierBuilder()
.key(publicKey)
.algorithm("SHA256withRSA")
.build();
var message = Bytes.from("Transferir 500€ a Alice");
var signature = signer.sign(message);
var encoded = signature.encode(BASE64); // listo para transporte
var isValid = verifier.verify(message, Bytes.from(encoded, BASE64));
Cargar claves, firmar, codificar en Base64, decodificar y verificar — en código legible y lineal.
Qué hace Bruce bajo el capó
Bruce no reemplaza la JCA. Es un wrapper delgado en puro Java alrededor de ella. Sin dependencias criptográficas externas, sin código nativo — solo el motor criptográfico de la JVM con una API humana encima. Esto es lo que obtienes:
SigneryVerifierson@FunctionalInterface. Hacen una sola cosa y son inmutables. Puedes pasarlos, almacenarlos en campos, usarlos en lambdas.Byteses el tipo universal de I/O de Bruce. Envuelvebyte[]con factory methods convenientes (Bytes.from(String),Bytes.from(Path),Bytes.from(InputStream)) y soporte de codificación (encode(BASE64),encode(HEX)). No más gestión manual de charset o Base64.- Los builders manejan la ceremonia de
getInstance/inituna vez en el momento de la construcción. Después, firmar y verificar son llamadas de una línea. - Las excepciones checked se capturan y se envuelven en
BruceExceptionunchecked, para que no contaminen las firmas de tus métodos.
¿Múltiples claves? Sin problema
Si gestionas firmas para múltiples partes, Bruce tiene las variantes
SignerByKey y VerifierByKey:
var signerByKey = signerBuilder()
.keys(Map.of("alice", alicePrivateKey, "bob", bobPrivateKey))
.algorithm("SHA256withRSA")
.buildByKey();
var verifierByKey = verifierBuilder()
.keys(Map.of("alice", alicePublicKey, "bob", bobPublicKey))
.algorithm("SHA256withRSA")
.buildByKey();
var signature = signerByKey.sign("alice", message);
var isValid = verifierByKey.verify("alice", message, signature);
Intenta hacer eso limpiamente con la JCA pura. Te espero.
Para empezar
Añade Bruce a tu proyecto:
<dependency>
<groupId>com.mirkocaserta.bruce</groupId>
<artifactId>bruce</artifactId>
<version>2.0.0</version>
</dependency>
O con Gradle:
implementation("com.mirkocaserta.bruce:bruce:2.0.0")
Bruce requiere Java 21+ y tiene cero dependencias externas.
La vida es demasiado corta para KeyPairGenerator.getInstance().