diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..52e3f3b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/client-libs-java-samples"] + path = submodules/client-libs-java-samples + url = https://github.com/Adscore/client-libs-java-samples diff --git a/README.md b/README.md index e0d01d0..76984c9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Various Java client libraries for utilization of APIs in AdScore.com -
Latest version: 1.0.3 - currently available features:
+
Latest version: 1.0.4 - currently available features:
1. SignatureVerifier
other languages:
@@ -11,11 +11,26 @@ Various Java client libraries for utilization of APIs in com.adscore client-libraries - 1.0.3 + 1.0.4 system - ${project.basedir}/libs/adscore-client-libraries-1.0.3.jar + ${project.basedir}/libs/adscore-client-libraries-1.0.4.jar ``` or as a Gradle: ```gradle -compile files('libs/adscore-client-libraries-1.0.3.jar') +compile files('libs/adscore-client-libraries-1.0.4.jar') ``` -Note that this is a thin jar, so in case of manual referencing (i.e. not via mvn central) you need to ensure yourself that dependencies are satisifed - for list of those please look at build.gradle. -

How to build library manually

If you want you can also build the library yourself. in order to do that you need to ensure: @@ -123,7 +136,8 @@ than you have at least few options of how to verify signatures: ```java - // Verify with base64 encoded key and without expiry checking + // Verify with base64 encoded key. + // (No expiry parameter, the default expiry time for requestTime and signatureTime is 60s) SignatureVerificationResult result = SignatureVerifier.verify( "BAYAXlNKGQFeU0oggAGBAcAAIAUdn1gbCBmA-u-kF--oUSuFw4B93piWC1Dn-D_1_6gywQAgEXCqgk2zPD6hWI1Y2rlrtV-21eIYBsms0odUEXNbRbA", @@ -145,11 +159,12 @@ than you have at least few options of how to verify signatures: "customer", "key_non_base64_encoded", false, // notify that we use non encoded key - 60, // signature cant be older than 1 min + 60, // signature cant be older than 1 min "73.109.57.137"); [..] // Verify against number of ip4 and ip6 addresses + //(No expiry parameter, the default expiry time for requestTime and signatureTime is 60s) result = SignatureVerifier.verify( "BAYAXlNKGQFeU0oggAGBAcAAIAUdn1gbCBmA-u-kF--oUSuFw4B93piWC1Dn-D_1_6gywQAgEXCqgk2zPD6hWI1Y2rlrtV-21eIYBsms0odUEXNbRbA", diff --git a/build.gradle b/build.gradle index 58214d3..7939add 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group 'com.adscore' -version '1.0.3' +version '1.0.4' sourceCompatibility = 1.8 diff --git a/settings.gradle b/settings.gradle index e7c482b..55e2e4f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'adscore-client-libraries' \ No newline at end of file +rootProject.name = 'client-libs-java' \ No newline at end of file diff --git a/src/main/java/com/adscore/signature/SignatureVerifier.java b/src/main/java/com/adscore/signature/SignatureVerifier.java index 0214668..184c27a 100644 --- a/src/main/java/com/adscore/signature/SignatureVerifier.java +++ b/src/main/java/com/adscore/signature/SignatureVerifier.java @@ -24,13 +24,6 @@ package com.adscore.signature; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Date; -import java.util.HashMap; -import java.util.StringJoiner; -import java.util.regex.Pattern; - /** * Entry point of AdScore signature verification library. It expose verify method allowing to verify * AdScore signature against given set of ipAddress(es) for given zone. @@ -39,36 +32,11 @@ */ public class SignatureVerifier { - // constants - - private static HashMap fieldIds = - new HashMap() { - { - put(0x00, new Field("requestTime", "ulong")); - put(0x01, new Field("signatureTime", "ulong")); - put(0x40, new Field(null, "ushort")); - put(0x80, new Field("masterSignType", "uchar")); - put(0x81, new Field("customerSignType", "uchar")); - put(0xC0, new Field("masterToken", "string")); - put(0xC1, new Field("customerToken", "string")); - put(0xC2, new Field("masterTokenV6", "string")); - put(0xC3, new Field("customerTokenV6", "string")); - } - }; - - private static HashMap results = - new HashMap() { - { - put("0", "ok"); - put("3", "junk"); - put("6", "proxy"); - put("9", "bot"); - } - }; - - // API + public static final int DEFAULT_EXPIRY_TIME_SEC = 60; /** + * Default request and signature expiration is set to 60s + * * @param signature the string which we want to verify * @param userAgent string with full description of user agent like 'Mozilla/5.0 (Linux; Android * 9; SM-J530F)...' @@ -81,8 +49,8 @@ public class SignatureVerifier { */ public static SignatureVerificationResult verify( String signature, String userAgent, String signRole, String key, String... ipAddresses) { - - return SignatureVerifier.verify(signature, userAgent, signRole, key, true, null, ipAddresses); + return SignatureVerifier.verify( + signature, userAgent, signRole, key, true, DEFAULT_EXPIRY_TIME_SEC, ipAddresses); } /** @@ -110,6 +78,8 @@ public static SignatureVerificationResult verify( } /** + * Default request and signature expiration is set to 60s + * * @param signature the string which we want to verify * @param userAgent string with full description of user agent like 'Mozilla/5.0 (Linux; Android * 9; SM-J530F)...' @@ -130,7 +100,13 @@ public static SignatureVerificationResult verify( String... ipAddresses) { return SignatureVerifier.verify( - signature, userAgent, signRole, key, isKeyBase64Encoded, null, ipAddresses); + signature, + userAgent, + signRole, + key, + isKeyBase64Encoded, + DEFAULT_EXPIRY_TIME_SEC, + ipAddresses); } /** @@ -156,413 +132,8 @@ public static SignatureVerificationResult verify( Integer expiry, String... ipAddresses) { - key = isKeyBase64Encoded ? keyDecode(key) : key; - - SignatureVerificationResult validationResult = new SignatureVerificationResult(); - - try { - HashMap data; - try { - data = parse4(signature); - } catch (BaseSignatureVerificationException exp) { - if (exp instanceof SignatureRangeException) { - data = parse3(signature); - } else { - - validationResult.setError(exp.getMessage()); - return validationResult; - } - } - - String signRoleToken = (String) data.get(signRole + "Token"); - if (signRoleToken == null || signRoleToken.length() == 0) { - - validationResult.setError("sign role signature mismatch"); - return validationResult; - } - - int signType = GeneralUtils.characterToInt(data.get(signRole + "SignType")); - - for (String ipAddress : ipAddresses) { - String token; - if (ipAddress == null || ipAddress.length() == 0) { - continue; - } - if (IpV6Utils.validate(ipAddress)) { - - if (!data.containsKey(signRole + "TokenV6")) { - continue; - } - token = (String) data.get(signRole + "TokenV6"); - ipAddress = IpV6Utils.abbreviate(ipAddress); - } else { - if (!data.containsKey(signRole + "Token")) { - continue; - } - - token = (String) data.get(signRole + "Token"); - } - - for (String result : results.keySet()) { - switch (signType) { - case 1: - String signatureBase = - getBase( - result, - GeneralUtils.characterToInt(data.get("requestTime")), - GeneralUtils.characterToInt(data.get("signatureTime")), - ipAddress, - userAgent); - - boolean isHashedDataEqualToToken = hashData(signatureBase, key).equals(token); - - if (isHashedDataEqualToToken) { - if (expiry != null - && GeneralUtils.characterToInt(data.get("signatureTime")) + expiry - < new Date().getTime() / 1000) { - validationResult.setExpired(true); - return validationResult; - } - - validationResult.setScore(Integer.valueOf(result)); - validationResult.setVerdict(results.get(result)); - validationResult.setIpAddress(ipAddress); - validationResult.setRequestTime( - Integer.parseInt(String.valueOf(data.get("requestTime")))); - validationResult.setSignatureTime( - Integer.parseInt(String.valueOf(data.get("signatureTime")))); - - return validationResult; - } - break; - case 2: - validationResult.setError("unsupported signature"); - return validationResult; - default: - validationResult.setError("unrecognized signature"); - return validationResult; - } - } - } - - validationResult.setError("no verdict"); - return validationResult; - - } catch (Exception exp) { - - validationResult.setError(exp.getMessage()); - return validationResult; - } - } - - // internals - - /** - * @param key in base64 format - * @return decoded key - */ - static String keyDecode(String key) { - return SignatureVerifier.atob(key); - } - - static String atob(String str) { - return new String(Base64.getMimeDecoder().decode(str.getBytes()), StandardCharsets.ISO_8859_1); - } - - static String padStart(String inputString, int length, char c) { - if (inputString.length() >= length) { - return inputString; - } - StringBuilder sb = new StringBuilder(); - while (sb.length() < length - inputString.length()) { - sb.append(c); - } - sb.append(inputString); - - return sb.toString(); - } - - static String getBase( - String verdict, int requestTime, int signatureTime, String ipAddress, String userAgent) { - StringJoiner joiner = new StringJoiner("\n"); - - return joiner - .add(verdict) - .add(String.valueOf(requestTime)) - .add(String.valueOf(signatureTime)) - .add(ipAddress) - .add(userAgent) - .toString(); - } - - private static String fromBase64(String data) { - return atob(data.replace('_', '/').replace('-', '+')); - } - - private static HashMap unpack(String format, String data) - throws SignatureVerificationException { - int formatPointer = 0; - int dataPointer = 0; - HashMap result = new HashMap<>(); - int instruction; - String quantifier; - int quantifierInt; - String label; - String currentData; - int i; - int currentResult; - - while (formatPointer < format.length()) { - instruction = GeneralUtils.charAt(format, formatPointer); - quantifier = ""; - formatPointer++; - - while ((formatPointer < format.length()) - && isCharMatches("[\\d\\*]", GeneralUtils.charAt(format, formatPointer))) { - quantifier += GeneralUtils.charAt(format, formatPointer); - formatPointer++; - } - if ("".equals(quantifier)) { - quantifier = "1"; - } - - StringBuilder labelSb = new StringBuilder(); - while ((formatPointer < format.length()) && (format.charAt(formatPointer) != '/')) { - labelSb.append(GeneralUtils.charAt(format, formatPointer++)); - } - label = labelSb.toString(); - - if (GeneralUtils.charAt(format, formatPointer) == '/') { - formatPointer++; - } - - switch (instruction) { - case 'c': - case 'C': - if ("*".equals(quantifier)) { - quantifierInt = data.length() - dataPointer; - } else { - quantifierInt = Integer.parseInt(quantifier, 10); - } - - currentData = GeneralUtils.substr(data, dataPointer, quantifierInt); - dataPointer += quantifierInt; - - for (i = 0; i < currentData.length(); i++) { - currentResult = GeneralUtils.charAt(currentData, i); - - if ((instruction == 'c') && (currentResult >= 128)) { - currentResult -= 256; - } - - String key = label + (quantifierInt > 1 ? (i + 1) : ""); - result.put(key, currentResult); - } - break; - case 'n': - if ("*".equals(quantifier)) { - quantifierInt = (data.length() - dataPointer) / 2; - } else { - quantifierInt = Integer.parseInt(quantifier, 10); - } - - currentData = GeneralUtils.substr(data, dataPointer, quantifierInt * 2); - dataPointer += quantifierInt * 2; - for (i = 0; i < currentData.length(); i += 2) { - currentResult = - (((GeneralUtils.charAt(currentData, i) & 0xFF) << 8) - + (GeneralUtils.charAt(currentData, i + 1) & 0xFF)); - - String key = label + (quantifierInt > 1 ? ((i / 2) + 1) : ""); - result.put(key, currentResult); - } - break; - case 'N': - if ("*".equals(quantifier)) { - quantifierInt = (data.length() - dataPointer) / 4; - } else { - quantifierInt = Integer.parseInt(quantifier, 10); - } - - currentData = GeneralUtils.substr(data, dataPointer, quantifierInt * 4); - dataPointer += quantifierInt * 4; - for (i = 0; i < currentData.length(); i += 4) { - currentResult = - (((GeneralUtils.charAt(currentData, i) & 0xFF) << 24) - + ((GeneralUtils.charAt(currentData, i + 1) & 0xFF) << 16) - + ((GeneralUtils.charAt(currentData, i + 2) & 0xFF) << 8) - + ((GeneralUtils.charAt(currentData, i + 3) & 0xFF))); - - String key = label + (quantifierInt > 1 ? ((i / 4) + 1) : ""); - result.put(key, currentResult); - } - break; - default: - throw new SignatureVerificationException( - String.format("Unknown format code:%s", String.valueOf(instruction))); - } - } - - return result; - } - - private static boolean isCharMatches(String regex, int formatChar) { - return Pattern.compile(regex).matcher(String.valueOf(formatChar)).matches(); - } - - private static HashMap parse3(String signature) - throws BaseSignatureVerificationException { - signature = fromBase64(signature); - if (!"".equals(signature)) { - throw new SignatureVerificationException("invalid base64 payload"); - } - - HashMap data1 = - unpack( - "Cversion/NrequestTime/NsignatureTime/CmasterSignType/nmasterTokenLength", signature); - - Integer version = (Integer) data1.get("version"); - - if (version != 3) { - throw new SignatureRangeException("unsupported version"); - } - - Long timestamp = (Long) data1.get("timestamp"); - if (timestamp > (new Date().getTime() / 1000)) { - throw new SignatureVerificationException("invalid timestamp (future time)"); - } - - Integer masterTokenLength = (Integer) data1.get("masterTokenLength"); - String masterToken = GeneralUtils.substr(signature, 12, masterTokenLength + 12); - data1.put("masterToken", masterToken); - - int s1, s2; - - if ((s1 = masterTokenLength) != (s2 = masterToken.length())) { - throw new SignatureVerificationException( - String.format("master token length mismatch (%s / %s)", s1, s2)); - } - - signature = GeneralUtils.substr(signature, masterTokenLength + 12); - - HashMap data2 = unpack("CcustomerSignType/ncustomerTokenLength", signature); - - Integer customerTokenLength = (Integer) data2.get("customerTokenLength"); - String customerToken = GeneralUtils.substr(signature, 3, customerTokenLength + 3); - data2.put("customerToken", customerToken); - - if ((s1 = customerTokenLength) != (s2 = customerToken.length())) { - throw new SignatureVerificationException( - String.format("customer token length mismatch (%s / %s)')", s1, s2)); - } - - data1.putAll(data2); - - return data1; - } - - private static Field fieldTypeDef(Integer fieldId, int i) { - if (fieldIds.get(fieldId) != null) { - return fieldIds.get(fieldId); - } - - String resultType = fieldIds.get(fieldId & 0xC0).getType(); - - String iStr = padStart(String.valueOf(i), 2, '0'); - String resultName = resultType + iStr; - - return new Field(resultName, resultType); - } - - private static HashMap parse4(String signature) - throws BaseSignatureVerificationException { - signature = fromBase64(signature); - - if (signature.length() == 0) { - throw new SignatureVerificationException("invalid base64 payload"); - } - - HashMap data = unpack("Cversion/CfieldNum", signature); - - int version = GeneralUtils.characterToInt(data.get("version")); - if (version != 4) { - throw new SignatureRangeException("unsupported version"); - } - signature = GeneralUtils.substr(signature, 2); - - int fieldNum = GeneralUtils.characterToInt(data.get("fieldNum")); - - for (int i = 0; i < fieldNum; ++i) { - HashMap header = unpack("CfieldId", signature); - - if (header.entrySet().size() == 0 || !header.containsKey("fieldId")) { - throw new SignatureVerificationException("premature end of signature 0x01"); - } - - Field fieldTypeDef = fieldTypeDef(GeneralUtils.characterToInt(header.get("fieldId")), i); - HashMap v = new HashMap<>(); - HashMap l; - - switch (fieldTypeDef.getType()) { - case "uchar": - v = unpack("Cx/Cv", signature); - if (v.containsKey("v")) { - data.put(fieldTypeDef.getName(), v.get("v")); - } else { - throw new SignatureVerificationException("premature end of signature 0x02"); - } - signature = GeneralUtils.substr(signature, 2); - break; - case "ushort": - v = unpack("Cx/nv", signature); - if (v.containsKey("v")) { - data.put(fieldTypeDef.getName(), v.get("v")); - } else { - throw new Error("premature end of signature 0x03"); - } - signature = GeneralUtils.substr(signature, 3); - break; - case "ulong": - v = unpack("Cx/Nv", signature); - if (v.containsKey("v")) { - data.put(fieldTypeDef.getName(), v.get("v")); - } else { - throw new Error("premature end of signature 0x04"); - } - signature = GeneralUtils.substr(signature, 5); - break; - case "string": - l = unpack("Cx/nl", signature); - if (!l.containsKey("l")) { - throw new Error("premature end of signature 0x05"); - } - if ((GeneralUtils.characterToInt(l.get("l")) & 0x8000) > 0) { - int newl = GeneralUtils.characterToInt(l.get("l")) & 0xFF; - l.put("l", newl); - } - - String newV = GeneralUtils.substr(signature, 3, GeneralUtils.characterToInt(l.get("l"))); - v.put("v", newV); - data.put(fieldTypeDef.getName(), newV); - - if (((String) v.get("v")).length() != GeneralUtils.characterToInt(l.get("l"))) { - throw new SignatureVerificationException("premature end of signature 0x06"); - } - - signature = GeneralUtils.substr(signature, 3 + GeneralUtils.characterToInt(l.get("l"))); - - break; - default: - throw new SignatureVerificationException("unsupported variable type"); - } - } - - data.remove(String.valueOf(fieldNum)); - - return data; - } - - private static String hashData(String data, String key) throws Exception { - return GeneralUtils.encode(key, data); + return new SignatureVerifierService() + .verifySignature( + signature, userAgent, signRole, key, isKeyBase64Encoded, expiry, ipAddresses); } } diff --git a/src/main/java/com/adscore/signature/SignatureVerifierService.java b/src/main/java/com/adscore/signature/SignatureVerifierService.java new file mode 100644 index 0000000..9d6cc9a --- /dev/null +++ b/src/main/java/com/adscore/signature/SignatureVerifierService.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2020 AdScore Technologies DMCC [AE] + * + * Licensed under MIT License; + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.adscore.signature; + +import java.util.Date; +import java.util.HashMap; +import java.util.StringJoiner; + +/** + * Core logic of signature verifier + * + * @author Łukasz Hyła + */ +public class SignatureVerifierService { + + private static final HashMap fieldIds = + new HashMap() { + { + put(0x00, new Field("requestTime", "ulong")); + put(0x01, new Field("signatureTime", "ulong")); + put(0x40, new Field(null, "ushort")); + put(0x80, new Field("masterSignType", "uchar")); + put(0x81, new Field("customerSignType", "uchar")); + put(0xC0, new Field("masterToken", "string")); + put(0xC1, new Field("customerToken", "string")); + put(0xC2, new Field("masterTokenV6", "string")); + put(0xC3, new Field("customerTokenV6", "string")); + } + }; + + private static final HashMap results = + new HashMap() { + { + put("0", "ok"); + put("3", "junk"); + put("6", "proxy"); + put("9", "bot"); + } + }; + + SignatureVerificationResult verifySignature( + String signature, + String userAgent, + String signRole, + String key, + boolean isKeyBase64Encoded, + Integer expiry, + String[] ipAddresses) { + key = isKeyBase64Encoded ? SignatureVerifierUtils.keyDecode(key) : key; + + SignatureVerificationResult validationResult = new SignatureVerificationResult(); + + try { + HashMap data; + try { + data = parse4(signature); + } catch (BaseSignatureVerificationException exp) { + if (exp instanceof SignatureRangeException) { + data = parse3(signature); + } else { + + validationResult.setError(exp.getMessage()); + return validationResult; + } + } + + String signRoleToken = (String) data.get(signRole + "Token"); + if (signRoleToken == null || signRoleToken.length() == 0) { + + validationResult.setError("sign role signature mismatch"); + return validationResult; + } + + int signType = SignatureVerifierUtils.characterToInt(data.get(signRole + "SignType")); + + for (String ipAddress : ipAddresses) { + String token; + if (ipAddress == null || ipAddress.length() == 0) { + continue; + } + if (IpV6Utils.validate(ipAddress)) { + + if (!data.containsKey(signRole + "TokenV6")) { + continue; + } + token = (String) data.get(signRole + "TokenV6"); + ipAddress = IpV6Utils.abbreviate(ipAddress); + } else { + if (!data.containsKey(signRole + "Token")) { + continue; + } + + token = (String) data.get(signRole + "Token"); + } + + int signatureTime = SignatureVerifierUtils.characterToInt(data.get("signatureTime")); + int requestTime = SignatureVerifierUtils.characterToInt(data.get("requestTime")); + + for (String result : results.keySet()) { + + switch (signType) { + case 1: + String signatureBase = + getBase(result, requestTime, signatureTime, ipAddress, userAgent); + + boolean isHashedDataEqualToToken = + SignatureVerifierUtils.encode(key, signatureBase).equals(token); + + if (isHashedDataEqualToToken) { + if (isExpired(expiry, signatureTime, requestTime)) { + validationResult.setExpired(true); + return validationResult; + } + + validationResult.setScore(Integer.valueOf(result)); + validationResult.setVerdict(results.get(result)); + validationResult.setIpAddress(ipAddress); + validationResult.setRequestTime( + Integer.parseInt(String.valueOf(data.get("requestTime")))); + validationResult.setSignatureTime( + Integer.parseInt(String.valueOf(data.get("signatureTime")))); + + return validationResult; + } + break; + case 2: + validationResult.setError("unsupported signature"); + return validationResult; + default: + validationResult.setError("unrecognized signature"); + return validationResult; + } + } + } + + validationResult.setError("no verdict"); + return validationResult; + + } catch (Exception exp) { + + validationResult.setError(exp.getMessage()); + return validationResult; + } + } + + /** + * @param expiry how long request and signature are valid (in seconds) + * @param signatureTime epoch time in seconds + * @param requestTime epoch time in seconds + * @return false if expiry is null. True if either signatureTime or requestTime expired, false + * otherwise. + */ + boolean isExpired(Integer expiry, int signatureTime, int requestTime) { + + if (expiry == null) { + // If expiry time not provided, neither signatureTime nor requestTime can be expired. + return false; + } + + long currentEpochInSeconds = new Date().getTime() / 1000; + + // Cast both times to long, because operating on int epoch seconds exceeds integer max value + // while adding higher dates (around 2035) + boolean isSignatureTimeExpired = (long) signatureTime + (long) expiry < currentEpochInSeconds; + boolean isRequestTimeExpired = (long) requestTime + (long) expiry < currentEpochInSeconds; + + return isSignatureTimeExpired || isRequestTimeExpired; + } + + String getBase( + String verdict, int requestTime, int signatureTime, String ipAddress, String userAgent) { + StringJoiner joiner = new StringJoiner("\n"); + + return joiner + .add(verdict) + .add(String.valueOf(requestTime)) + .add(String.valueOf(signatureTime)) + .add(ipAddress) + .add(userAgent) + .toString(); + } + + private HashMap parse3(String signature) + throws BaseSignatureVerificationException { + signature = SignatureVerifierUtils.fromBase64(signature); + if (!"".equals(signature)) { + throw new SignatureVerificationException("invalid base64 payload"); + } + + UnpackResult unpackResult = + Unpacker.unpack( + "Cversion/NrequestTime/NsignatureTime/CmasterSignType/nmasterTokenLength", signature); + + Integer version = (Integer) unpackResult.getData().get("version"); + + if (version != 3) { + throw new SignatureRangeException("unsupported version"); + } + + Long timestamp = (Long) unpackResult.getData().get("timestamp"); + if (timestamp > (new Date().getTime() / 1000)) { + throw new SignatureVerificationException("invalid timestamp (future time)"); + } + + Integer masterTokenLength = (Integer) unpackResult.getData().get("masterTokenLength"); + String masterToken = SignatureVerifierUtils.substr(signature, 12, masterTokenLength + 12); + unpackResult.getData().put("masterToken", masterToken); + + int s1, s2; + + if ((s1 = masterTokenLength) != (s2 = masterToken.length())) { + throw new SignatureVerificationException( + String.format("master token length mismatch (%s / %s)", s1, s2)); + } + + signature = SignatureVerifierUtils.substr(signature, masterTokenLength + 12); + + HashMap data2 = + Unpacker.unpack("CcustomerSignType/ncustomerTokenLength", signature).getData(); + + Integer customerTokenLength = (Integer) data2.get("customerTokenLength"); + String customerToken = SignatureVerifierUtils.substr(signature, 3, customerTokenLength + 3); + data2.put("customerToken", customerToken); + + if ((s1 = customerTokenLength) != (s2 = customerToken.length())) { + throw new SignatureVerificationException( + String.format("customer token length mismatch (%s / %s)')", s1, s2)); + } + + unpackResult.getData().putAll(data2); + + return unpackResult.getData(); + } + + private Field fieldTypeDef(Integer fieldId, int i) { + if (fieldIds.get(fieldId) != null) { + return fieldIds.get(fieldId); + } + + String resultType = fieldIds.get(fieldId & 0xC0).getType(); + + String iStr = SignatureVerifierUtils.padStart(String.valueOf(i), 2, '0'); + String resultName = resultType + iStr; + + return new Field(resultName, resultType); + } + + private HashMap parse4(String signature) + throws BaseSignatureVerificationException { + signature = SignatureVerifierUtils.fromBase64(signature); + + if (signature.length() == 0) { + throw new SignatureVerificationException("invalid base64 payload"); + } + + HashMap data = Unpacker.unpack("Cversion/CfieldNum", signature).getData(); + + int version = SignatureVerifierUtils.characterToInt(data.get("version")); + if (version != 4) { + throw new SignatureRangeException("unsupported version"); + } + signature = SignatureVerifierUtils.substr(signature, 2); + + int fieldNum = SignatureVerifierUtils.characterToInt(data.get("fieldNum")); + + for (int i = 0; i < fieldNum; ++i) { + HashMap header = Unpacker.unpack("CfieldId", signature).getData(); + + if (header.entrySet().size() == 0 || !header.containsKey("fieldId")) { + throw new SignatureVerificationException("premature end of signature 0x01"); + } + + Field fieldTypeDef = + fieldTypeDef(SignatureVerifierUtils.characterToInt(header.get("fieldId")), i); + HashMap v = new HashMap<>(); + HashMap l; + + switch (fieldTypeDef.getType()) { + case "uchar": + v = Unpacker.unpack("Cx/Cv", signature).getData(); + if (v.containsKey("v")) { + data.put(fieldTypeDef.getName(), v.get("v")); + } else { + throw new SignatureVerificationException("premature end of signature 0x02"); + } + signature = SignatureVerifierUtils.substr(signature, 2); + break; + case "ushort": + v = Unpacker.unpack("Cx/nv", signature).getData(); + if (v.containsKey("v")) { + data.put(fieldTypeDef.getName(), v.get("v")); + } else { + throw new Error("premature end of signature 0x03"); + } + signature = SignatureVerifierUtils.substr(signature, 3); + break; + case "ulong": + v = Unpacker.unpack("Cx/Nv", signature).getData(); + if (v.containsKey("v")) { + data.put(fieldTypeDef.getName(), v.get("v")); + } else { + throw new Error("premature end of signature 0x04"); + } + signature = SignatureVerifierUtils.substr(signature, 5); + break; + case "string": + l = Unpacker.unpack("Cx/nl", signature).getData(); + if (!l.containsKey("l")) { + throw new Error("premature end of signature 0x05"); + } + if ((SignatureVerifierUtils.characterToInt(l.get("l")) & 0x8000) > 0) { + int newl = SignatureVerifierUtils.characterToInt(l.get("l")) & 0xFF; + l.put("l", newl); + } + + String newV = + SignatureVerifierUtils.substr( + signature, 3, SignatureVerifierUtils.characterToInt(l.get("l"))); + v.put("v", newV); + data.put(fieldTypeDef.getName(), newV); + + if (((String) v.get("v")).length() != SignatureVerifierUtils.characterToInt(l.get("l"))) { + throw new SignatureVerificationException("premature end of signature 0x06"); + } + + signature = + SignatureVerifierUtils.substr( + signature, 3 + SignatureVerifierUtils.characterToInt(l.get("l"))); + + break; + default: + throw new SignatureVerificationException("unsupported variable type"); + } + } + + data.remove(String.valueOf(fieldNum)); + + return data; + } +} diff --git a/src/main/java/com/adscore/signature/GeneralUtils.java b/src/main/java/com/adscore/signature/SignatureVerifierUtils.java similarity index 70% rename from src/main/java/com/adscore/signature/GeneralUtils.java rename to src/main/java/com/adscore/signature/SignatureVerifierUtils.java index b862e3a..7dae0f5 100644 --- a/src/main/java/com/adscore/signature/GeneralUtils.java +++ b/src/main/java/com/adscore/signature/SignatureVerifierUtils.java @@ -25,15 +25,18 @@ package com.adscore.signature; import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.regex.Pattern; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; /** - * Couple of general purpose utilities created while porting from JS version of library + * General-purpose utilities, that help with string manipulations, encoding/decoding adscore key + * etc. * * @author Łukasz Hyła */ -class GeneralUtils { +class SignatureVerifierUtils { /** Method behaves same as js function: "str".substr(startIdx,length) */ static String substr(String str, int startIdx, int length) { @@ -75,4 +78,37 @@ static String encode(String key, String data) throws Exception { byte[] digest = mac.doFinal(data.getBytes()); return new String(digest, StandardCharsets.ISO_8859_1); } + + /** + * @param key in base64 format + * @return decoded key + */ + static String keyDecode(String key) { + return atob(key); + } + + static String atob(String str) { + return new String(Base64.getMimeDecoder().decode(str.getBytes()), StandardCharsets.ISO_8859_1); + } + + static String padStart(String inputString, int length, char c) { + if (inputString.length() >= length) { + return inputString; + } + StringBuilder sb = new StringBuilder(); + while (sb.length() < length - inputString.length()) { + sb.append(c); + } + sb.append(inputString); + + return sb.toString(); + } + + static boolean isCharMatches(String regex, int formatChar) { + return Pattern.compile(regex).matcher(String.valueOf(formatChar)).matches(); + } + + static String fromBase64(String data) { + return SignatureVerifierUtils.atob(data.replace('_', '/').replace('-', '+')); + } } diff --git a/src/main/java/com/adscore/signature/UnpackResult.java b/src/main/java/com/adscore/signature/UnpackResult.java new file mode 100644 index 0000000..1175346 --- /dev/null +++ b/src/main/java/com/adscore/signature/UnpackResult.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 AdScore Technologies DMCC [AE] + * + * Licensed under MIT License; + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.adscore.signature; + +import java.util.HashMap; + +/** + * This class is a wrapper for a result of unpack() method. Contains data end error message in + * separate variables + * + *

Copyright (c) 2020 AdScore Technologies DMCC [AE] + * + * @author lhyla + */ +class UnpackResult { + + private HashMap data; + private String error; + + UnpackResult(HashMap data) { + this.data = data; + } + + UnpackResult(String error) { + this.error = error; + } + + HashMap getData() { + return data; + } + + String getError() { + return error; + } +} diff --git a/src/main/java/com/adscore/signature/Unpacker.java b/src/main/java/com/adscore/signature/Unpacker.java new file mode 100644 index 0000000..b94cc0c --- /dev/null +++ b/src/main/java/com/adscore/signature/Unpacker.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2020 AdScore Technologies DMCC [AE] + * + * Licensed under MIT License; + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.adscore.signature; + +import java.util.HashMap; + +/** + * Copyright (c) 2020 AdScore Technologies DMCC [AE] + * + * @author lhyla + */ +class Unpacker { + + /** + * Unpacks version field from the binary string + * + * @param signatureDecodedFromBase64 Signature already decoded from Base64 + * @return Version as as single integer. + */ + static Integer unpackVersion(String signatureDecodedFromBase64) { + return (Integer) + Unpacker.unpack("Cversion", signatureDecodedFromBase64).getData().get("version"); + } + + /** + * Unpacks data from a binary string into the respective format. + * + * @param format fields that have to be unpacked from data, forward slash separated. + * @param data Binary string, already decoded from Base64 + * @return UnpackResult object which contains unpacked data as a hash map, where key is a name of + * the field. if result contains non-null error message then it means that unpacking failed. + * Data hash map is null then. + */ + static UnpackResult unpack(String format, String data) { + int formatPointer = 0; + int dataPointer = 0; + HashMap resultMap = new HashMap<>(); + int instruction; + String quantifier; + int quantifierInt; + String label; + String currentData; + int i; + int currentResult; + + while (formatPointer < format.length()) { + instruction = SignatureVerifierUtils.charAt(format, formatPointer); + quantifier = ""; + formatPointer++; + + while ((formatPointer < format.length()) + && SignatureVerifierUtils.isCharMatches( + "[\\d\\*]", SignatureVerifierUtils.charAt(format, formatPointer))) { + quantifier += SignatureVerifierUtils.charAt(format, formatPointer); + formatPointer++; + } + if ("".equals(quantifier)) { + quantifier = "1"; + } + + StringBuilder labelSb = new StringBuilder(); + while ((formatPointer < format.length()) && (format.charAt(formatPointer) != '/')) { + labelSb.append(SignatureVerifierUtils.charAt(format, formatPointer++)); + } + label = labelSb.toString(); + + if (SignatureVerifierUtils.charAt(format, formatPointer) == '/') { + formatPointer++; + } + + switch (instruction) { + case 'c': + case 'C': + if ("*".equals(quantifier)) { + quantifierInt = data.length() - dataPointer; + } else { + quantifierInt = Integer.parseInt(quantifier, 10); + } + + currentData = SignatureVerifierUtils.substr(data, dataPointer, quantifierInt); + dataPointer += quantifierInt; + + for (i = 0; i < currentData.length(); i++) { + currentResult = SignatureVerifierUtils.charAt(currentData, i); + + if ((instruction == 'c') && (currentResult >= 128)) { + currentResult -= 256; + } + + String key = label + (quantifierInt > 1 ? (i + 1) : ""); + resultMap.put(key, currentResult); + } + break; + case 'n': + if ("*".equals(quantifier)) { + quantifierInt = (data.length() - dataPointer) / 2; + } else { + quantifierInt = Integer.parseInt(quantifier, 10); + } + + currentData = SignatureVerifierUtils.substr(data, dataPointer, quantifierInt * 2); + dataPointer += quantifierInt * 2; + for (i = 0; i < currentData.length(); i += 2) { + currentResult = + (((SignatureVerifierUtils.charAt(currentData, i) & 0xFF) << 8) + + (SignatureVerifierUtils.charAt(currentData, i + 1) & 0xFF)); + + String key = label + (quantifierInt > 1 ? ((i / 2) + 1) : ""); + resultMap.put(key, currentResult); + } + break; + case 'N': + if ("*".equals(quantifier)) { + quantifierInt = (data.length() - dataPointer) / 4; + } else { + quantifierInt = Integer.parseInt(quantifier, 10); + } + + currentData = SignatureVerifierUtils.substr(data, dataPointer, quantifierInt * 4); + dataPointer += quantifierInt * 4; + for (i = 0; i < currentData.length(); i += 4) { + currentResult = + (((SignatureVerifierUtils.charAt(currentData, i) & 0xFF) << 24) + + ((SignatureVerifierUtils.charAt(currentData, i + 1) & 0xFF) << 16) + + ((SignatureVerifierUtils.charAt(currentData, i + 2) & 0xFF) << 8) + + ((SignatureVerifierUtils.charAt(currentData, i + 3) & 0xFF))); + + String key = label + (quantifierInt > 1 ? ((i / 4) + 1) : ""); + resultMap.put(key, currentResult); + } + break; + default: + return new UnpackResult( + String.format("Unknown format code:%s", String.valueOf(instruction))); + } + } + + return new UnpackResult(resultMap); + } +} diff --git a/submodules/client-libs-java-samples b/submodules/client-libs-java-samples new file mode 160000 index 0000000..ba17f82 --- /dev/null +++ b/submodules/client-libs-java-samples @@ -0,0 +1 @@ +Subproject commit ba17f825f841fb5b7804d50208a434c522edb269