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.javarshk-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):
| # | Clave | Valor | Condición |
|---|---|---|---|
| 1 | nVersion | "150" (constante SIFEN_CURRENT_VERSION) | Siempre |
| 2 | Id | CDC del documento (44 chars) | Siempre |
| 3 | dFeEmiDE | Fecha de emisión hex-encoded (ver sección 2) | Siempre |
| 4 | dRucRec | RUC del receptor | Solo si iNatRec == 1 (persona física/jurídica paraguaya) |
| 4 | dNumIDRec | Número de ID del receptor, o "0" si no aplica | Si iNatRec != 1 y (iTiOpe == 4 o dNumIDRec == null) → "0" |
| 5 | dTotGralOpe | Total general de la operación, o "0" para tipo DE=7 | Siempre |
| 6 | dTotIVA | Total IVA, o "0" si iTImp != 1 y iTImp != 5 | Siempre |
| 7 | cItems | Cantidad de ítems (gCamItemList.size()) | Siempre |
| 8 | DigestValue | DigestValue de la firma, transformado (ver sección 3) | Siempre |
| 9 | IdCSC | Identificador del CSC (de SifenConfig) | Siempre |
Nota:
cHashQRNO entra en elLinkedHashMap. 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
Tentre 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:
-
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) -
Se codifica en Base64 estándar (no URL-safe, no sin padding):
Base64.getEncoder().encode(rawBytes)// → byte[] que representa la cadena Base64, ej: "abc123+/==" -
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
md5por error en el código, pero usaSHA-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
URLEncodera los valores. Verificar si el CSC o algún campo puede contener caracteres especiales.