22
33import java .nio .charset .StandardCharsets ;
44import java .security .*;
5+ import java .security .spec .X509EncodedKeySpec ;
56import java .time .Instant ;
67import java .util .Base64 ;
78import java .util .HashMap ;
89import java .util .Map ;
910import java .security .spec .PKCS8EncodedKeySpec ;
10- import javax .crypto .Mac ;
11- import javax .crypto .spec .SecretKeySpec ;
1211
1312import org .json .JSONObject ;
1413
@@ -41,6 +40,10 @@ public class SnapBi {
4140 private String deviceId ;
4241 private String debugId ;
4342 private String timeStamp ;
43+ private String signature ;
44+ private String notificationUrlPath ;
45+ private String notificationPayload ;
46+
4447
4548 public SnapBi (String paymentMethod ) {
4649 this .paymentMethod = paymentMethod ;
@@ -59,6 +62,10 @@ public static SnapBi qris() {
5962 return new SnapBi ("qris" );
6063 }
6164
65+ public static SnapBi notification () {
66+ return new SnapBi ("" );
67+ }
68+
6269 public SnapBi withAccessTokenHeader (Map <String , String > headers ) {
6370 this .accessTokenHeader .putAll (headers );
6471 return this ;
@@ -114,6 +121,26 @@ public SnapBi withDebugId(String debugId) {
114121 return this ;
115122 }
116123
124+ public SnapBi withSignature (String signature ) {
125+ this .signature = signature ;
126+ return this ;
127+ }
128+
129+ public SnapBi withTimeStamp (String timeStamp ) {
130+ this .timeStamp = timeStamp ;
131+ return this ;
132+ }
133+
134+ public SnapBi withNotificationUrlPath (String notificationUrlPath ) {
135+ this .notificationUrlPath = notificationUrlPath ;
136+ return this ;
137+ }
138+
139+ public SnapBi withNotificationPayload (String notificationPayload ) {
140+ this .notificationPayload = notificationPayload ;
141+ return this ;
142+ }
143+
117144 public JSONObject createPayment (String externalId ) throws Exception {
118145 this .apiPath = setupCreatePaymentApiPath (this .paymentMethod );
119146 return createConnection (externalId );
@@ -134,6 +161,43 @@ public JSONObject getStatus(String externalId) throws Exception {
134161 return createConnection (externalId );
135162 }
136163
164+ public Boolean isWebhookNotificationVerified () throws Exception {
165+ if (SnapBiConfig .getSnapBiPublicKey () == null || SnapBiConfig .getSnapBiPublicKey ().trim ().isEmpty ()) {
166+ throw new IllegalStateException
167+ ("The public key is null, You need to set the public key from SnapBiConfig.' .\n " +
168+ "For more details contact support at support@midtrans.com if you have any questions." );
169+ }
170+ String minifiedBody = minifyJson (this .notificationPayload );
171+ MessageDigest digest = MessageDigest .getInstance ("SHA-256" );
172+ byte [] hashedNotificationBodyJsonString = digest .digest (minifiedBody .getBytes ());
173+ String hashedNotificationBodyJsonStringHex = bytesToHex (hashedNotificationBodyJsonString )
174+ .toLowerCase ();
175+ String rawStringDataToVerifyAgainstSignature = "POST" +
176+ ":" +
177+ this .notificationUrlPath +
178+ ":" +
179+ hashedNotificationBodyJsonStringHex +
180+ ":" +
181+ this .timeStamp ;
182+ Signature verifier = Signature .getInstance ("SHA256withRSA" );
183+ verifier .initVerify (getPublicKey (SnapBiConfig .getSnapBiPublicKey ()));
184+ verifier .update (rawStringDataToVerifyAgainstSignature .getBytes (StandardCharsets .UTF_8 ));
185+ boolean isSignatureVerified = verifier .verify (Base64 .getDecoder ().decode (this .signature ));
186+
187+ return isSignatureVerified ;
188+ }
189+
190+ private static PublicKey getPublicKey (String publicKeyString ) throws Exception {
191+ String publicKeyPEM = publicKeyString
192+ .replace ("-----BEGIN PUBLIC KEY-----" , "" )
193+ .replace ("-----END PUBLIC KEY-----" , "" )
194+ .replaceAll ("\\ s" , "" );
195+ byte [] keyBytes = Base64 .getDecoder ().decode (publicKeyPEM );
196+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec (keyBytes );
197+ KeyFactory keyFactory = KeyFactory .getInstance ("RSA" );
198+ return keyFactory .generatePublic (keySpec );
199+ }
200+
137201 public JSONObject getAccessToken () throws Exception {
138202 Map <String , String > snapBiAccessTokenHeader = buildAccessTokenHeader (this .timeStamp );
139203 Map <String , String > openApiPayload = new HashMap <>();
@@ -172,9 +236,9 @@ private Map<String, String> buildSnapBiTransactionHeader(String externalId, Stri
172236 snapBiTransactionHeader .put ("Accept" , "application/json" );
173237 snapBiTransactionHeader .put ("X-PARTNER-ID" , SnapBiConfig .getSnapBiPartnerId ());
174238 snapBiTransactionHeader .put ("X-EXTERNAL-ID" , externalId );
175- snapBiTransactionHeader .put ("X-DEVICE-ID" , this .deviceId != null ? this .deviceId : "" );
239+ snapBiTransactionHeader .put ("X-DEVICE-ID" , this .deviceId != null ? this .deviceId : "" );
176240 snapBiTransactionHeader .put ("CHANNEL-ID" , SnapBiConfig .getSnapBiChannelId ());
177- snapBiTransactionHeader .put ("debug-id" , this .debugId != null ? this .debugId : "" );
241+ snapBiTransactionHeader .put ("debug-id" , this .debugId != null ? this .debugId : "" );
178242 snapBiTransactionHeader .put ("Authorization" , "Bearer " + this .accessToken );
179243 snapBiTransactionHeader .put ("X-TIMESTAMP" , timeStamp );
180244 snapBiTransactionHeader .put ("X-SIGNATURE" , SnapBi .getSymmetricSignatureHmacSh512 (
@@ -229,6 +293,7 @@ public static String getSymmetricSignatureHmacSh512(String accessToken, Map<Stri
229293 throw new RuntimeException ("Error generating symmetric signature" , e );
230294 }
231295 }
296+
232297 private static String minifyJson (String json ) {
233298 // Minify JSON by removing whitespace
234299 return json .replaceAll ("\\ s+" , "" );
@@ -245,13 +310,15 @@ private static byte[] hmacSha512(String payload, String key) throws NoSuchAlgori
245310 mac .init (secretKeySpec );
246311 return mac .doFinal (payload .getBytes (StandardCharsets .UTF_8 ));
247312 }
313+
248314 private static String bytesToHex (byte [] bytes ) {
249315 StringBuilder sb = new StringBuilder ();
250316 for (byte b : bytes ) {
251317 sb .append (String .format ("%02x" , b ));
252318 }
253319 return sb .toString ();
254320 }
321+
255322 public static String getAsymmetricSignatureSha256WithRsa (String clientId , String timeStamp , String privateKey ) throws Exception {
256323 String stringToSign = clientId + "|" + timeStamp ;
257324
0 commit comments