package com.bidding.supplier.security.openapi; import com.alibaba.fastjson2.JSON; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * Supplier account API codec delivered to the customer for payload decryption * and BCrypt password verification. */ public class BiddingSupplierAccountApiCodec { private static final String PROTOCOL = "BSA"; private static final String VERSION = "v1"; private static final int IV_LENGTH = 12; private static final int GCM_TAG_BITS = 128; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); private final String keyId; private final String prefix; private final byte[] aesKey; public BiddingSupplierAccountApiCodec(String keyId, String aesKeyBase64) { if (keyId == null || !keyId.matches("^[A-Za-z0-9_-]{1,32}$")) { throw new IllegalArgumentException("Invalid supplier account API key id"); } try { this.aesKey = Base64.getDecoder().decode(aesKeyBase64 == null ? "" : aesKeyBase64.trim()); } catch (IllegalArgumentException exception) { throw new IllegalArgumentException("Invalid supplier account API AES key", exception); } if (aesKey.length != 32) { throw new IllegalArgumentException("Supplier account API AES key must be 32 bytes"); } this.keyId = keyId; this.prefix = PROTOCOL + "." + VERSION + "." + keyId + "."; } public String decryptPayload(String payload) { try { String[] parts = parsePayload(payload); byte[] iv = Base64.getUrlDecoder().decode(parts[3]); byte[] ciphertextAndTag = Base64.getUrlDecoder().decode(parts[4]); if (iv.length != IV_LENGTH || ciphertextAndTag.length <= GCM_TAG_BITS / 8) { throw new IllegalArgumentException("Invalid supplier account API payload"); } Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv)); return new String(cipher.doFinal(ciphertextAndTag), StandardCharsets.UTF_8); } catch (IllegalArgumentException exception) { throw exception; } catch (Exception exception) { throw new IllegalArgumentException("Unable to decrypt supplier account API payload", exception); } } public T decryptPayload(String payload, Class targetType) { if (targetType == null) { throw new IllegalArgumentException("Target type is required"); } return JSON.parseObject(decryptPayload(payload), targetType); } /** * Generate a BCrypt hash for a new or changed customer-platform password. */ public String encodePassword(String rawPassword) { if (rawPassword == null) { throw new IllegalArgumentException("Raw password is required"); } return PASSWORD_ENCODER.encode(rawPassword); } public boolean matchesPassword(String rawPassword, String bcryptPasswordHash) { if (rawPassword == null || bcryptPasswordHash == null || bcryptPasswordHash.isBlank()) { return false; } try { return PASSWORD_ENCODER.matches(rawPassword, bcryptPasswordHash); } catch (IllegalArgumentException exception) { return false; } } String encryptPayload(Object payload) { try { byte[] iv = new byte[IV_LENGTH]; SECURE_RANDOM.nextBytes(iv); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv)); byte[] ciphertextAndTag = cipher.doFinal(JSON.toJSONBytes(payload)); Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); return prefix + encoder.encodeToString(iv) + "." + encoder.encodeToString(ciphertextAndTag); } catch (Exception exception) { throw new IllegalStateException("Unable to encrypt supplier account API payload", exception); } } private String[] parsePayload(String payload) { if (payload == null || !payload.startsWith(prefix)) { throw new IllegalArgumentException("Invalid supplier account API payload"); } String[] parts = payload.split("\\.", 5); if (parts.length != 5 || !PROTOCOL.equals(parts[0]) || !VERSION.equals(parts[1]) || !keyId.equals(parts[2])) { throw new IllegalArgumentException("Invalid supplier account API payload"); } return parts; } }