SNAP
Asymmetric
Data Preparation
| No | Data | Description | Example |
|---|---|---|---|
| 1 | Method | Method on each API: POST, GET, DELETE, PUT, PATCH | POST |
| 2 | Url | URL on each api (exclude the base domain) and must be exists “/” | /payment |
| 3 | Body | Minify and Hash the request body with SHA-256 | |
| 4 | Timestamp | Transaction date time, in format YYYY-MM-DDTHH:mm:ss+07:00. Time must be in GMT+7 (Jakarta time) | 2022-11-30T09:45:35+07:00 |
Compose String to Sign
<Method>:<Url Relative>:LowerCase(HexEncode(SHA-256(Minify(<Body>)))):< Timestamp>
Example
POST:/https://dev-api.cashin.co.id/pg/fo/payment/notify:dfdfdfd23432432432432432sdfdsfdsf3434:2024-10-30T10:30:35+07:00
Result of StringToSign applying SHA-256 with RSA-2048 encryption using PKCS#8 or PKCS#1 private key, and then encode the result to base64.
Example Result
15+wYJy0EFEuSaAmWXW4U7VxWbCSWxBO4XQn09fVAq1VkohB6d6Sw1/tpyWMJYOU+sdQTnFf7+oPiFlwtZy/yg==
Example detail implementation
Generate Signature
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.StringReader;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
class SignatureGeneratorSNAPJava {
// Main test method to generate a digital signature
@Test
void testGenerateSignature() throws Exception {
// 1. Get a Signature instance for SHA256 with RSA algorithm
Signature signatureProvider = Signature.getInstance("SHA256withRSA");
// 2. Generate the string that will be signed
String stringToSign = generateStringToSign();
// 3. Base64 encoded private key (in PKCS#8 format)
String privateKeyBase64Encoded = "TUlJRXZRSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2N3Z2dTakFnRUFBb0lCQVFES01ycW13cC9ZRWI0YwpJZks1V3BtZ3BqV2tSYlBJb042NGpKaU9OMTM3NnluSXExZ1hUUUZWU2RydTd4NndxMS9xbksxNVZ1Uzh3d0EyCmZCWFB0ZS9SMVh5SU9ocEZHcUVRK0lIcGxOdmY2ejIxd1RHVnRkM2RlY2I1bE5RaHB6Y2NaVUplLzloZitGR2MKQWllWGFGZlRYVStqZ1VhYkE0YjVTcEl5eG5ia08yRjZtL3hSUFhoWklpSjJjZGF2THBIR3hVMGF3VzQ3R3pGUAp3dUVZaG51RjVvNVlicFh0bUhUVlJWRXNlUDlpMXhhNnBOT2Zkd2gwTkpwWXV0U1IyQ1pHTTFJTjRMTkY5TTN1CjhBaldiSmNVRjNUN3p4ZHVzN3ZxNCs3UTR1M0cySE1KVHdGOGEzaUx6ZjA5ZVNsNENBekhlVzdmMzJCcGorUGoKZUNRWHMrdGRBZ01CQUFFQ2dnRUFNbnFFTFJZcEZDYm56SHV3TGxSL1JvTkFJSXp6TTNydjUyaFlQS2IzYkdrNgo0dmxkMG1GYjNCaEdVMEM0cTNmNVIrZktDaUVtbFRaa2d2V3E5MUpQTnhaQ1V2c1liRmlYemRZUWpza21ORXAwCit6OXFPdHN1c3JoUWM2WVRhSHhucm92Y0REMzRuWXJ2aXRCaDVITjgrNHhQMGJ5NExRSnVLNlhSaG1JVU0rbHAKNzFxR2dzeEVlUTczQTZVdjBmQU0rNzNFTzFuZHdiSlpRQWw2UHNzazltdm1LU1hoSnlsdnNZYTlRU1ZsZnI5MQprVjFPc20rRjZzZGNyTjEzbm52ZFVVblgvWkVOOFcxZ0NlYlVoR2FYcTZOYWo3dVYrLzFlWkR6ZTU0QW8xVDFYClI5bTU1UG1qNGw2RGptQ2FaYmxrRlRYWjYvalZnR0FZVi85V2UvakR5UUtCZ1FEOUg0UFltZ3paMmNTNEtiNTkKRlRicnB4WUU4eHhOcXhiMU1PSXFpdERYUDZ6QVdDNGlCanlhY1R3cVhmRnp6R0w3TXFGYnNHTlJXWmlkQ1U4Mgo4dEthUjY0ZHdzQ0w5K041QmdJWDFFajUzOEkxeTBGVmNXVWZmWlZQNGZIUkdqN2NvUEE2bHhLV3NwY2x5ZTZ5CjZyNVJGaWVkTFh3VWZLVzdYMlRSaDl4NlNRS0JnUURNZndzVG5TeEIzU0grNHhRQUdLY3kxNG1pNFd6R0dadTUKTHNwMWc0RjBzdzBLd3FsbUFVQ3pVVXZDWEVoVWpWQUtjcVZmUFZPbWEzaWQ2NXFRWDRJSkNsdkVjcm9NK01lOQo0eU5vVnFJMTY3SlVlc2FOVDBkV1VQVitydjRmOXl4bWc1UnIvRG5nWTZoMkkySlZNQjUyRlN1VGt3TWUyQTFiCkpyUm41Uy9JZFFLQmdBN000UEN4VVQ4THRLendBK3RjYWpHdE8yUmVzckQzNkFlK0svdEEwcVZEVzk2RFNXOUQKM2hkSnQ3TGllMVBDQjZlWWRrYmVNWEI0Ukt3cDl5L0hVdmtpWjlQbHAweUgyZzBoaE0zUDQ5UjRlT3FjVDkybgpHN3FUcFQ5ZWZyMHpRNm15MzFrTnJGQ1RjTE1NODZBU2liNUVCVnp6WStYdXcrSUkyZVJHaFdYQkFvR0FMOTVwClRGMFZXQkdZZko3Uk5yaU9vdk1iVDlwdTZPeGpySFNNaGZlVG5TcXdtZDA1WkJ4VTllVEkxQ1ZmOVJMak1wN2wKb1NhczV4N3ZMQ0JUblFvT1dXbG9VOUw3UkVBVUdab0sxc2k0emVCdmJTRVVTMDNNaUNNSlpHODRJMmxycGsrVQp0YlprSWVlU0xwMVh6bE1tUGtQMFlHWWdhOTd0T1hJVTl1RUtUMlVDZ1lFQTJkakdmNnU2K2wyWlJXVm1wc0tQCndqb21tVHVyN29YVFIyUjQ3Rm50ZXRtZFZYbGtsRUJzYkk1M1VzbzNrYmFBRGh6cGcvdEVrR0F4M0FsQldoYk8KMmZhVVdyL2FjMDRjRnBqUGN2OEs2ZFNvU1pYMW92bFE3ZWdoSzczSVNCZVptQkpabHAreXhmTnZtTWdmZlZyYgpobmIybXVQTzBqOU5nZHR2MkVaby8wOD0";
// 4. Initialize the signature provider with the private key
PrivateKey privateKey = getPrivateKey(privateKeyBase64Encoded);
signatureProvider.initSign(privateKey);
// 5. Update the signature provider with the data to be signed
signatureProvider.update(stringToSign.getBytes());
// 6. Generate the digital signature
byte[] signatureByteArray = signatureProvider.sign();
// 7. Encode the signature in Base64 for easy transmission/storage
String signatureResult = Base64.getEncoder().encodeToString(signatureByteArray);
// 8. Print the resulting signature
System.out.println("Generated Signature:");
System.out.println(signatureResult);
}
// Helper method to generate the string that will be signed
private String generateStringToSign() throws NoSuchAlgorithmException {
String httpMethod = "POST";
String url = "https://dev-api.cashin.co.id/pg/fo/payment/notify";
String payload = "{"originalPartnerReferenceNo":"01960f60-e57e-7447-a627-9c38330f1372","originalReferenceNo":"PAY174401475395059252","merchantId":"MCH173693014536297166","amount":{"value":"12000.00","currency":"IDR"},"latestTransactionStatus":"00","createdTime":"2025-04-07T08:32:58.654197119Z","finishedTime":"2025-04-07T08:32:57Z"}";
// Create SHA-256 digest of the payload
MessageDigest digestSHA256 = MessageDigest.getInstance("SHA-256");
String timestamp = "2025-04-07T15:32:58+07:00";
// Format: HTTPMethod:URL:SHA256HashOfPayload:Timestamp
return httpMethod + ":/" + url + ":" + hexEncodeRequest(payload, digestSHA256) + ":" + timestamp;
}
// Helper method to convert private key from Base64 to PrivateKey object
public static PrivateKey getPrivateKey(String key) throws Exception {
// Decode the base64 encoded key
String decodedKey = new String(Base64.getDecoder().decode(key));
// Read all lines from the key
BufferedReader reader = new BufferedReader(new StringReader(decodedKey));
StringBuilder pkcs8Lines = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
pkcs8Lines.append(line);
}
// Remove the "BEGIN" and "END" lines, as well as any whitespace
String pkcs8Pem = pkcs8Lines.toString()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\s+", "");
// Decode to bytes
byte[] pkcs8EncodedBytes = Base64.getDecoder().decode(pkcs8Pem.getBytes());
// Extract the private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8EncodedBytes);
return keyFactory.generatePrivate(keySpec);
}
// Helper method to hex encode the request payload
private String hexEncodeRequest(String payload, MessageDigest digest) {
// Remove any newlines, returns or tabs from payload
String cleanedPayload = payload.replaceAll("[\n\r\t]", "");
// Create SHA-256 hash of the payload
byte[] hashBytes = digest.digest(cleanedPayload.getBytes());
// Convert the byte array to hexadecimal string
String hexString = bytesToHex(hashBytes);
// Return lowercase hexadecimal string
return hexString.toLowerCase();
}
// Helper method to convert byte array to hex string
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
// Convert each byte to two hex characters
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
Verify Signature
import org.junit.Test;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Locale;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SignatureVerifierJava {
@Test
public void testValidateSignature() throws Exception {
String stringToSign = generateStringToSign();
String signatureToCheck =
"Ds14wwl4cf1RRca9rdTKY9x5NkBvEX7kDJ3K6VCftWdo2g0UdUvItLGiMuS/bBrcUYO6/9h0jS1MXsnpZtUROH24lx1GFaDsSPAr8UryoEFN33RM5L8LbOGsYmKhkCRPG42g96nyaMHeCKhm/Z2/ESD2iQTuyOBPYS/BOxhY0wq1S0OT5eWvFiMvxSUcf4af2rFaYi+gT3B1fxCitFScFk/1I3C9Ic1ZD1q9hEV5NSYGOJlpl0kI92+3oPzbejMuxyYUDJCaQByCwbf47vwOmno9gD9SOTnOTRpx2ziVlaU0cnNBnA6GHFiGCToSJKfqfnzQhWKUOgFn7BhMXjrbYQ==";
String publicKeyBase64Encoded =
"TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF5aks2cHNLZjJCRytIQ0h5dVZxWgpvS1kxcEVXenlLRGV1SXlZampkZCsrc3B5S3RZRjAwQlZVbmE3dThlc0t0ZjZweXRlVmJrdk1NQU5ud1Z6N1h2CjBkVjhpRG9hUlJxaEVQaUI2WlRiMytzOXRjRXhsYlhkM1huRytaVFVJYWMzSEdWQ1h2L1lYL2hSbkFJbmwyaFgKMDExUG80Rkdtd09HK1VxU01zWjI1RHRoZXB2OFVUMTRXU0lpZG5IV3J5NlJ4c1ZOR3NGdU94c3hUOExoR0laNwpoZWFPV0c2VjdaaDAxVVZSTEhqL1l0Y1d1cVRUbjNjSWREU2FXTHJVa2RnbVJqTlNEZUN6UmZUTjd2QUkxbXlYCkZCZDArODhYYnJPNzZ1UHUwT0x0eHRoekNVOEJmR3Q0aTgzOVBYa3BlQWdNeDNsdTM5OWdhWS9qNDNna0Y3UHIKWFFJREFRQUI";
byte[] byteSignedData = stringToSign.getBytes();
byte[] byteSignature = Base64.getDecoder().decode(signatureToCheck.getBytes());
Signature signatureProvider = Signature.getInstance("SHA256withRSA");
PublicKey publicKeyObject = getPublicKey(publicKeyBase64Encoded);
signatureProvider.initVerify(publicKeyObject);
signatureProvider.update(byteSignedData);
boolean isValidSignature = signatureProvider.verify(byteSignature);
assertTrue(isValidSignature);
}
private String generateStringToSign() throws NoSuchAlgorithmException {
String httpMethod = "POST";
String url = "https://dev-api.cashin.co.id/pg/fo/payment/notify ";
String payload = "{\"originalPartnerReferenceNo\":\"01960f60-e57e-7447-a627-9c38330f1372\",\"originalReferenceNo\":\"PAY174401475395059252\",\"merchantId\":\"MCH173693014536297166\",\"amount\":{\"value\":\"12000.00\",\"currency\":\"IDR\"},\"latestTransactionStatus\":\"00\",\"createdTime\":\"2025-04-07T08:32:58.654197119Z\",\"finishedTime\":\"2025-04-07T08:32:57Z\"}";
MessageDigest digestSHA256 = MessageDigest.getInstance("SHA-256");
String timestamp = "2025-04-07T15:32:58+07:00";
return httpMethod + ":/" + url + ":" + hexEncodeRequest(payload, digestSHA256) + ":" + timestamp;
}
private PublicKey getPublicKey(String publicKey) throws Exception {
String pubKeyPEM = new String(Base64.getDecoder().decode(publicKey.getBytes()))
.replace("-----BEGIN PUBLIC KEY-----
", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\s+", "");
byte[] base64Key = Base64.getDecoder().decode(pubKeyPEM.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(base64Key);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
private String hexEncodeRequest(String payload, MessageDigest digest) {
return bytesToHex(digest.digest(
payload.replaceAll("[\n\r\t]", "")
.getBytes()))
.toLowerCase(Locale.getDefault());
}
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
private String bytesToHex(byte[] bytes) {
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);
}
}