Cassa-DSU/src/puntocassa/utils/QrCryptoService.java
francescods 4db09682f7 modifica codifica stringa QR
l'algoritmo rimane lo stesso, ma non usiamo più un JSON ma la concatenazione di IV in Base64, un carattere | (pipe) e il payload in Base64 (dopo la criptazione con AES-GCM con chiave da 256 bit)
il payload attuale contiene solo il codice fiscale, un carattere | (pipe) e la data di scadenza del QR
2025-11-05 09:06:05 +01:00

143 lines
5.4 KiB
Java

/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package puntocassa.utils;
/**
*
* @author assis
*/
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Base64;
import javax.crypto.spec.IvParameterSpec;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.time.*;
import java.util.*;
public class QrCryptoService {
private static final byte[] AES_KEY = hexToBytes("3031323334353637383961626364656630313233343536373839616263646566");
private static final Duration CLOCK_SKEW = Duration.ofSeconds(60);
public static String createEncryptedQr(String uid, OffsetDateTime expiresAt) throws GeneralSecurityException {
String payload = uid + "|" + expiresAt.toString();
byte[] iv = SecureRandom.getInstanceStrong().generateSeed(16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(AES_KEY, "AES"), new IvParameterSpec(iv));
byte[] encrypted = cipher.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);
String ivBase64 = Base64.getEncoder().encodeToString(iv);
return ivBase64 + "|" + encryptedBase64;
}
public static List<String> decryptAndValidate(String qrRaw) {
List<String> resultData = new ArrayList<>();
try {
String[] parts = qrRaw.split("\\|", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Formato QR non valido. Atteso: IV|payload");
}
byte[] iv = Base64.getDecoder().decode(parts[0]);
byte[] ciphertext = Base64.getDecoder().decode(parts[1]);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(AES_KEY, "AES"), new IvParameterSpec(iv));
byte[] decrypted = cipher.doFinal(ciphertext);
String payload = new String(decrypted, StandardCharsets.UTF_8);
ValidationResult result = validatePayload(payload);
if (result.valid()) {
resultData.add("VALID");
resultData.add(result.uid());
} else {
resultData.add("NOT VALID");
resultData.add("Errore validazione QR code: " + result.reason());
}
} catch (Exception e) {
resultData.add("NOT VALID");
resultData.add("Errore di decifratura o struttura non valida. QR letto: " + qrRaw);
resultData.add(e.toString());
}
return resultData;
}
private static ValidationResult validatePayload(String payload) {
try {
String[] parts = payload.split("\\|", 2);
if (parts.length != 2) {
return ValidationResult.invalid("Payload malformato: atteso UID|dataOra");
}
String uid = parts[0].trim();
String dateTimeStr = parts[1].trim();
if (!uid.matches("[A-Z0-9]{16}")) {
return ValidationResult.invalid("Formato UID non valido");
}
OffsetDateTime expiresAt = OffsetDateTime.parse(dateTimeStr);
Instant now = Instant.now();
Instant exp = expiresAt.toInstant();
if (now.minus(CLOCK_SKEW).isAfter(exp)) {
return ValidationResult.invalid("QR scaduto");
}
return ValidationResult.valid(uid, exp);
} catch (Exception e) {
return ValidationResult.invalid("Errore di validazione QR Code: Payload malformato o data non valida");
}
}
public static byte[] hexToBytes(String hex) {
return new BigInteger(hex, 16).toByteArray().length == 33
? Arrays.copyOfRange(new BigInteger(hex, 16).toByteArray(), 1, 33)
: new BigInteger(hex, 16).toByteArray();
}
public record ValidationResult(boolean valid, String reason, String uid, Instant expiresAt) {
public static ValidationResult valid(String uid, Instant expiresAt) {
return new ValidationResult(true, null, uid, expiresAt);
}
public static ValidationResult invalid(String reason) {
return new ValidationResult(false, reason, null, null);
}
}
public static void main(String[] args) throws Exception {
List<String> dataResult = new ArrayList<>();
OffsetDateTime now = OffsetDateTime.now().withNano(0);
OffsetDateTime expiration = now.plusSeconds(CLOCK_SKEW.getSeconds());
//String qr = createEncryptedQr("TSTTST91A48A271O", expiration);
//String qr = "eyJpdiI6Ik0xTVJvR3dMbm40UGFMalFEVUh5M1E9PSIsImRhdGEiOiJSVERTTkFURi8wK0lrekFGVVRaSkMrb1Y5QWVZcFRnMFFVKzgzY25QUVgrM2ZMVkRMV3kyOFRsMnBIQTlBRWVGTFQvK0pxSWNtblBYRnBjMys5T0tVWHJpc3FQc1VHazUxRzFpSERSOFJ1eEFCSWhPaTZlamlkdHU4d090WmRzY2F2Y0FRbmNESTd3bGxJNWgvOFFmZnc9PSJ9";
String qr = "Z8j6FyrmwmSTBYs4iOWTEg==|WKTaAvEqlC+RYW3Ix4ftJcqgDriqdk7pv+U5Aclhmgb70n8y9447wRV6MXW5ikPN";
System.out.println("QR generato:\n" + qr + "\n");
dataResult = decryptAndValidate(qr);
System.out.println("Verifica:\n" + dataResult);
}
}