Saltar al contenido principal

SIFEN Java — Referencia de Generación del QR

Extraído directamente del código fuente Java. Archivos fuente:

  • rshk-jsifen-maven/src/main/java/com/roshka/sifen/core/beans/DocumentoElectronico.java (líneas 379–417)
  • rshk-jsifen-maven/src/main/java/com/roshka/sifen/internal/util/SifenUtil.java
  • rshk-jsifen-maven/src/main/java/com/roshka/sifen/internal/Constants.java

1. Orden exacto de los campos en la cadena del QR

El mapa se construye con LinkedHashMap (orden de inserción garantizado):

#ClaveValorCondición
1nVersion"150" (constante SIFEN_CURRENT_VERSION)Siempre
2IdCDC del documento (44 chars)Siempre
3dFeEmiDEFecha de emisión hex-encoded (ver sección 2)Siempre
4dRucRecRUC del receptorSolo si iNatRec == 1 (persona física/jurídica paraguaya)
4dNumIDRecNúmero de ID del receptor, o "0" si no aplicaSi iNatRec != 1 y (iTiOpe == 4 o dNumIDRec == null) → "0"
5dTotGralOpeTotal general de la operación, o "0" para tipo DE=7Siempre
6dTotIVATotal IVA, o "0" si iTImp != 1 y iTImp != 5Siempre
7cItemsCantidad de ítems (gCamItemList.size())Siempre
8DigestValueDigestValue de la firma, transformado (ver sección 3)Siempre
9IdCSCIdentificador del CSC (de SifenConfig)Siempre

Nota: cHashQR NO entra en el LinkedHashMap. Se calcula aparte y se concatena al final.


2. Formato exacto de la fecha (dFeEmiDE)

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
// ...
SifenUtil.bytesToHex(
this.getgDatGralOpe().getdFeEmiDE()
.format(formatter)
.getBytes(StandardCharsets.UTF_8)
)

La cadena de fecha antes de hex-encodear es:

2024-03-15T14:30:00
  • Lleva la T entre fecha y hora (literal en el patrón: 'T').
  • Formato: yyyy-MM-dd'T'HH:mm:ss
  • Sí lleva guiones en la parte de fecha.
  • Sí lleva segundos (ss), NO milisegundos.
  • Esa cadena se convierte a bytes UTF-8 y luego a hex lowercase.

Ejemplo:

"2024-03-15T14:30:00"
→ getBytes(UTF_8) → [0x32, 0x30, 0x32, 0x34, ...]
→ bytesToHex() → "323032342d30332d31355431343a33303a3030"

3. Función exacta aplicada al DigestValue

Código Java transcripto (líneas 409–410):

byte[] digestValue = Base64.getEncoder().encode(
((Reference) signedInfo.getReferences().get(0)).getDigestValue()
);
queryParams.put("DigestValue", SifenUtil.bytesToHex(digestValue));

Proceso paso a paso:

  1. Se obtiene el DigestValue RAW (bytes del SHA-256 del nodo <DE>) desde la firma XMLDSig:

    ((Reference) signedInfo.getReferences().get(0)).getDigestValue()
    // → byte[] de 32 bytes (SHA-256 raw)
  2. Se codifica en Base64 estándar (no URL-safe, no sin padding):

    Base64.getEncoder().encode(rawBytes)
    // → byte[] que representa la cadena Base64, ej: "abc123+/=="
  3. Esos bytes Base64 se convierten a hex lowercase:

    SifenUtil.bytesToHex(base64Bytes)
    // → "616263313233...22

Resumen: DigestValue = HEX( BASE64( SHA256_raw_bytes ) ) No es solo Base64, ni solo hex — es hex del string Base64.

SifenUtil.bytesToHex (transcripción):

public static String bytesToHex(byte[] bytes) {
char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
  • Output: hex lowercase (0123456789abcdef).

4. Separador & en la cadena del hash

SifenUtil.buildUrlParams (transcripción):

public static String buildUrlParams(HashMap<String, String> params) {
if (params.isEmpty()) return "";

StringBuilder paramsString = new StringBuilder();
for (Map.Entry<String, String> param : params.entrySet()) {
paramsString.append(param.getKey())
.append("=")
.append(param.getValue())
.append("&");
}
return paramsString.substring(0, paramsString.length() - 1);
}

Comportamiento confirmado:

  • Los parámetros se unen con & entre ellos: nVersion=150&Id=...&dFeEmiDE=...
  • El & final es eliminado (substring(0, length - 1)).
  • La cadena resultante (urlParamsString) no termina en &.

Cálculo del hash QR (líneas 413–414):

String urlParamsString = SifenUtil.buildUrlParams(queryParams);
String hashedParams = SifenUtil.sha256Hex(urlParamsString + sifenConfig.getCSC());
  • El hash se calcula sobre: [params sin cHashQR][CSC] (sin & entre los params y el CSC).
  • El CSC se concatena directamente sin separador.

URL final (línea 416):

return sifenConfig.getUrlConsultaQr() + urlParamsString + "&cHashQR=" + hashedParams;

Estructura completa:

https://ekuatia.set.gov.py/consultas/qr?nVersion=150&Id=...&dFeEmiDE=...&dRucRec=...
&dTotGralOpe=...&dTotIVA=...&cItems=...&DigestValue=...&IdCSC=...&cHashQR=...

5. SifenUtil.sha256Hex (transcripción)

public static String sha256Hex(String input) {
try {
MessageDigest md5 = MessageDigest.getInstance("SHA-256");
byte[] digest = md5.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(Integer.toHexString((b & 0xFF) | 0x100), 1, 3);
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
  • Input se procesa como bytes UTF-8.
  • Output: SHA-256 en hex lowercase, 64 caracteres.
  • Nota: la variable se llama md5 por error en el código, pero usa SHA-256.

6. Pseudocódigo completo (PHP-compatible)

// 1. Formatear fecha
$fecha = $dFeEmiDE->format('Y-m-d\TH:i:s'); // "2024-03-15T14:30:00"
$fechaHex = bin2hex($fecha); // hex lowercase de los bytes UTF-8

// 2. Obtener DigestValue
$digestRaw = $signatureDigestBytes; // 32 bytes SHA-256 raw de la firma
$digestB64 = base64_encode($digestRaw); // Base64 estándar con padding
$digestHex = bin2hex($digestB64); // hex del string Base64

// 3. Construir params (en orden)
$params = [
'nVersion' => '150',
'Id' => $cdc,
'dFeEmiDE' => $fechaHex,
'dRucRec' => $rucReceptor, // o dNumIDRec según iNatRec
'dTotGralOpe' => (string) $totGral,
'dTotIVA' => (string) $totIVA,
'cItems' => (string) count($items),
'DigestValue' => $digestHex,
'IdCSC' => $idCSC,
];

// 4. Construir query string (sin urlencode — Java no la aplica)
$queryString = implode('&', array_map(
fn($k, $v) => "$k=$v",
array_keys($params),
$params
));

// 5. Hash
$cHashQR = hash('sha256', $queryString . $csc); // sin & entre queryString y CSC

// 6. URL final
$url = $urlConsultaQr . $queryString . '&cHashQR=' . $cHashQR;

Advertencia sobre URL encoding: El código Java no aplica URLEncoder a los valores. Verificar si el CSC o algún campo puede contener caracteres especiales.