@@ -0,0 +1,147 @@
|
||||
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> T decryptPayload(String payload, Class<T> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user