148 lines
5.2 KiB
Plaintext
148 lines
5.2 KiB
Plaintext
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;
|
|
}
|
|
}
|