Skip to content

Commit

Permalink
extracted header-magic as enum (bunq#93)
Browse files Browse the repository at this point in the history
BunqHeader
  • Loading branch information
tubbynl committed Jun 17, 2018
1 parent 7ed15ad commit 4be80e5
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 125 deletions.
61 changes: 12 additions & 49 deletions src/main/java/com/bunq/sdk/http/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,40 +61,12 @@ public class ApiClient {
SESSION_SERVER_URL
);

/**
* Header constants.
*/
public static final String HEADER_ATTACHMENT_DESCRIPTION = "X-Bunq-Attachment-Description";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_USER_AGENT = "User-Agent";
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
public static final String HEADER_LANGUAGE = "X-Bunq-Language";
public static final String HEADER_REGION = "X-Bunq-Region";
public static final String HEADER_REQUEST_ID = "X-Bunq-Client-Request-Id";
public static final String HEADER_GEOLOCATION = "X-Bunq-Geolocation";
private static final String HEADER_SIGNATURE = "X-Bunq-Client-Signature";
private static final String HEADER_AUTHENTICATION = "X-Bunq-Client-Authentication";
private static final String HEADER_RESPONSE_ID_LOWER_CASE = "x-bunq-client-response-id";
private static final String HEADER_RESPONSE_ID_UPPER_CASE = "X-Bunq-Client-Response-Id";

/**
* Field constants.
*/
private static final String FIELD_ERROR = "Error";
private static final String FIELD_ERROR_DESCRIPTION = "error_description";

/**
* Header value to disable the cache control.
*/
public static final String CACHE_CONTROL_NONE = "no-cache";

/**
* Prefix for bunq's own headers.
*/
private static final String USER_AGENT_BUNQ = "bunq-sdk-java/0.13.1";
public static final String LANGUAGE_EN_US = "en_US";
public static final String REGION_NL_NL = "nl_NL";
public static final String GEOLOCATION_ZERO = "0 0 0 0 000";
private static final String SCHEME_HTTPS = "https";

/**
Expand Down Expand Up @@ -193,9 +165,9 @@ public BunqResponseRaw post(

BunqRequestBody bunqRequestBody = BunqRequestBody.create(ContentType.JSON.getMediaType(), requestBodyBytes);

if (customHeaders.containsKey(HEADER_CONTENT_TYPE)) {
if (customHeaders.containsKey(BunqHeader.contentType.getHeader())) {
bunqRequestBody = BunqRequestBody.create(
MediaType.parse(customHeaders.get(HEADER_CONTENT_TYPE)),
MediaType.parse(customHeaders.get(BunqHeader.contentType.getHeader())),
requestBodyBytes
);
}
Expand Down Expand Up @@ -262,13 +234,13 @@ private void setHeaders(BunqRequestBuilder requestBuilder, Map<String, String> c

/**
*/
private void setDefaultHeaders(Request.Builder httpEntity) {
httpEntity.addHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NONE);
httpEntity.addHeader(HEADER_USER_AGENT, getVersion());
httpEntity.addHeader(HEADER_LANGUAGE, LANGUAGE_EN_US);
httpEntity.addHeader(HEADER_REGION, REGION_NL_NL);
httpEntity.addHeader(HEADER_REQUEST_ID, UUID.randomUUID().toString());
httpEntity.addHeader(HEADER_GEOLOCATION, GEOLOCATION_ZERO);
private void setDefaultHeaders(BunqRequestBuilder httpEntity) {
BunqHeader.cacheControl.addTo(httpEntity);
BunqHeader.userAgent.addTo(httpEntity);
BunqHeader.language.addTo(httpEntity);
BunqHeader.region.addTo(httpEntity);
BunqHeader.clientRequestId.addTo(httpEntity,UUID.randomUUID().toString());
BunqHeader.geolocation.addTo(httpEntity);
}

/**
Expand All @@ -285,8 +257,8 @@ private void setSessionHeaders(BunqRequestBuilder requestBuilder) {
String sessionToken = apiContext.getSessionToken();

if (sessionToken != null) {
requestBuilder.addHeader(HEADER_AUTHENTICATION, sessionToken);
requestBuilder.addHeader(HEADER_SIGNATURE, generateSignature(requestBuilder));
BunqHeader.clientAuthentication.addTo(requestBuilder,sessionToken);
BunqHeader.clientSignature.addTo(requestBuilder,generateSignature(requestBuilder));
}
}

Expand All @@ -297,12 +269,6 @@ private String generateSignature(BunqRequestBuilder requestBuilder) {
apiContext.getInstallationContext().getKeyPairClient());
}

/**
*/
private String getVersion() {
return USER_AGENT_BUNQ;
}

/**
*/
private BunqResponseRaw createBunqResponseRaw(Response response)
Expand All @@ -320,10 +286,7 @@ private BunqResponseRaw createBunqResponseRaw(Response response)
*/
private static String getResponseId(Response response) {
Map<String, String> headerMap = getHeadersMap(response);

if (headerMap.containsKey(HEADER_RESPONSE_ID_LOWER_CASE)) {
return headerMap.get(HEADER_RESPONSE_ID_LOWER_CASE);
} else return headerMap.getOrDefault(HEADER_RESPONSE_ID_UPPER_CASE, ERROR_COULD_NOT_DETERMINE_RESPONSE_ID);
return BunqHeader.clientResponseId.getOrDefault(headerMap,ERROR_COULD_NOT_DETERMINE_RESPONSE_ID);
}

/**
Expand Down
23 changes: 13 additions & 10 deletions src/main/java/com/bunq/sdk/http/BunqBasicHeader.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
package com.bunq.sdk.http;

import okhttp3.Response;

public class BunqBasicHeader {
private String name;
private String value;
private final BunqHeader name;
private final String value;

public static BunqBasicHeader get(BunqHeader header,Response response) {
return new BunqBasicHeader(header,response.header(header.getHeader()));
}

/**
*/
public BunqBasicHeader(String name, String value) {
this(BunqHeader.parse(name).get(),value);
}

public BunqBasicHeader(BunqHeader name, String value) {
this.name = name;
this.value = value;
}

/**
*/
public String getName() {
public BunqHeader getName() {
return name;
}

/**
*/
public String getValue() {
return value;
}

}
75 changes: 75 additions & 0 deletions src/main/java/com/bunq/sdk/http/BunqHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.bunq.sdk.http;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

public enum BunqHeader {
attachmentDescription("X-Bunq-Attachment-Description"),
cacheControl("Cache-Control","no-cache"),
contentType("Content-Type"),
clientAuthentication("X-Bunq-Client-Authentication"),
clientEncryptionHMAC("X-Bunq-Client-Encryption-Hmac"),
clientEncryptionIV("X-Bunq-Client-Encryption-Iv"),
clientEncryptionKey("X-Bunq-Client-Encryption-Key"),
clientRequestId("X-Bunq-Client-Request-Id"),
clientSignature("X-Bunq-Client-Signature"),
clientResponseId("X-Bunq-Client-Response-Id"),
geolocation("X-Bunq-Geolocation","0 0 0 0 000"),
language("X-Bunq-Language","en_US"),
region("X-Bunq-Region","nl_NL"),
serverSignature("X-Bunq-Server-Signature"),
userAgent("User-Agent","bunq-sdk-java/0.13.1");

private static final String PREFIX = "X-Bunq-";

private final String header;
private final String defaultValue;

BunqHeader(String header) {
this(header,null);
}

BunqHeader(String header, String defaultValue) {
this.header = header;
this.defaultValue = defaultValue;
}

public static Optional<BunqHeader> parse(String value) {
return Stream.of(values()).filter(h->h.equals(value)).findAny();
}

public String getHeader() {
return header;
}

public String getDefaultValue() {
return defaultValue;
}

public void addTo(Map<String,String> headers, String value) {
headers.put(getHeader(),value!=null?value:getDefaultValue());
}

public void addTo(BunqRequestBuilder requestBuilder) {
addTo(requestBuilder,null);
}

public void addTo(BunqRequestBuilder requestBuilder, String value) {
requestBuilder.addHeader(getHeader(),value!=null?value:getDefaultValue());
}

public boolean equals(String header) {
return getHeader().equalsIgnoreCase(header);
}

public boolean isBunq() {
return getHeader().startsWith(PREFIX);
}

public String getOrDefault(Map<String,String> headers,String defaultValue) {
Optional<String> key = headers.keySet().stream().filter(this::equals).findAny();
return key.map(headers::get).orElse(defaultValue);
}
}
73 changes: 13 additions & 60 deletions src/main/java/com/bunq/sdk/security/SecurityUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import com.bunq.sdk.context.ApiContext;
import com.bunq.sdk.exception.BunqException;
import com.bunq.sdk.exception.UncaughtExceptionError;
import com.bunq.sdk.http.ApiClient;
import com.bunq.sdk.http.BunqBasicHeader;
import com.bunq.sdk.http.BunqRequestBuilder;
import com.bunq.sdk.http.HttpMethod;
import com.bunq.sdk.http.*;
import okhttp3.Headers;
import okhttp3.Response;
import okio.BufferedSink;
Expand Down Expand Up @@ -38,8 +35,6 @@
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -72,14 +67,6 @@ public final class SecurityUtils {
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int KEY_PAIR_GENERATOR_KEY_SIZE = 2048;

/**
* Encryption-specific headers.
*/
private static final String HEADER_CLIENT_ENCRYPTION_HMAC = "X-Bunq-Client-Encryption-Hmac";
private static final String HEADER_CLIENT_ENCRYPTION_IV = "X-Bunq-Client-Encryption-Iv";
private static final String HEADER_CLIENT_ENCRYPTION_KEY = "X-Bunq-Client-Encryption-Key";
private static final String HEADER_SERVER_SIGNATURE = "X-Bunq-Server-Signature";

/**
* The MAC algorithm to use for calculating and verifying the HMAC.
*/
Expand Down Expand Up @@ -139,26 +126,14 @@ public final class SecurityUtils {
/**
* Delimiter constants for building the data to sign.
*/
private static final String HEADER_NAME_PREFIX_X_BUNQ = "X-Bunq-";
private static final String DELIMITER_METHOD_PATH = " ";
private static final String DELIMITER_HEADER_NAME_AND_VALUE = ": ";

/**
* Regex constants.
*/
private static final String REGEX_FOR_LOWERCASE_HEADERS = "(-[a-z])";

/**
* The index of the first item in an array.
*/
private static final int INDEX_FIRST = 0;

/**
* Substring constants.
*/
private static final int SUBSTRING_BEGIN_INDEX_FIRST_CHAR = 0;
private static final int SUBSTRING_END_INDEX_FIRST_CHAR = 1;

/**
*/
private SecurityUtils() {
Expand Down Expand Up @@ -315,7 +290,7 @@ private static void addHeaderClientEncryptionKey(ApiContext apiContext, SecretKe
cipher.init(Cipher.ENCRYPT_MODE, apiContext.getInstallationContext().getPublicKeyServer());
byte[] keyEncrypted = cipher.doFinal(key.getEncoded());
String keyEncryptedEncoded = Base64.getEncoder().encodeToString(keyEncrypted);
customHeaders.put(HEADER_CLIENT_ENCRYPTION_KEY, keyEncryptedEncoded);
BunqHeader.clientEncryptionKey.addTo(customHeaders,keyEncryptedEncoded);
} catch (GeneralSecurityException exception) {
throw new BunqException(exception.getMessage());
}
Expand All @@ -324,8 +299,7 @@ private static void addHeaderClientEncryptionKey(ApiContext apiContext, SecretKe
private static void addHeaderClientEncryptionIv(byte[] initializationVector, Map<String,
String> customHeaders) {
String initializationVectorEncoded = Base64.getEncoder().encodeToString(initializationVector);

customHeaders.put(HEADER_CLIENT_ENCRYPTION_IV, initializationVectorEncoded);
BunqHeader.clientEncryptionIV.addTo(customHeaders,initializationVectorEncoded);
}

private static byte[] encryptRequestBytes(byte[] requestBytes, SecretKey key,
Expand Down Expand Up @@ -358,7 +332,7 @@ private static void addHeaderClientEncryptionHmac(byte[] requestBytes,
bufferedSink.close();
byte[] hmac = mac.doFinal();
String hmacEncoded = Base64.getEncoder().encodeToString(hmac);
customHeaders.put(HEADER_CLIENT_ENCRYPTION_HMAC, hmacEncoded);
BunqHeader.clientEncryptionHMAC.addTo(customHeaders,hmacEncoded);
} catch (GeneralSecurityException | IOException exception) {
throw new BunqException(exception.getMessage());
}
Expand Down Expand Up @@ -415,9 +389,9 @@ private static String generateRequestHeadersSortedString(BunqRequestBuilder bunq
return Arrays.stream(bunqRequestBuilder.getAllHeaderAsArray())
.filter(
header ->
header.getName().startsWith(HEADER_NAME_PREFIX_X_BUNQ) ||
header.getName().equals(ApiClient.HEADER_CACHE_CONTROL) ||
header.getName().equals(ApiClient.HEADER_USER_AGENT)
header.getName().isBunq() ||
header.getName().equals(BunqHeader.cacheControl) ||
header.getName().equals(BunqHeader.userAgent)
)
.map(header -> header.getName() + DELIMITER_HEADER_NAME_AND_VALUE + header.getValue())
.sorted()
Expand Down Expand Up @@ -496,10 +470,8 @@ public static void validateResponseSignature(
response.headers()
);
Signature signature = getSignatureInstance();
BunqBasicHeader headerServerSignature = new BunqBasicHeader(
HEADER_SERVER_SIGNATURE,
response.header(HEADER_SERVER_SIGNATURE)
);
BunqBasicHeader headerServerSignature = BunqBasicHeader.get(BunqHeader.serverSignature,response);

byte[] serverSignatureBase64Bytes = headerServerSignature.getValue().getBytes();
byte[] serverSignatureDecoded = Base64.getDecoder().decode(serverSignatureBase64Bytes);
verifyDataSigned(signature, keyPublicServer, responseBytes, serverSignatureDecoded);
Expand All @@ -514,14 +486,10 @@ private static byte[] getResponseBytes(
List<BunqBasicHeader> allResponseHeader = new ArrayList<>();

for (int i = INDEX_FIRST; i < allHeader.names().size(); i++) {
if (allHeader.name(i).equals(HEADER_SERVER_SIGNATURE)) {
if (BunqHeader.serverSignature.getHeader().equals(allHeader.name(i))) {
continue;
}

allResponseHeader.add(new BunqBasicHeader(
getHeaderNameCorrectlyCased(allHeader.name(i)),
allHeader.get(allHeader.name(i))
));
allResponseHeader.add(new BunqBasicHeader(allHeader.name(i),allHeader.get(allHeader.name(i))));
}

try {
Expand All @@ -539,21 +507,6 @@ private static byte[] getResponseBytes(
return outputStream.toByteArray();
}

private static String getHeaderNameCorrectlyCased(String headerName) {
headerName = headerName.toLowerCase();
headerName = headerName.substring(SUBSTRING_BEGIN_INDEX_FIRST_CHAR, SUBSTRING_END_INDEX_FIRST_CHAR).toUpperCase()
+ headerName.substring(SUBSTRING_END_INDEX_FIRST_CHAR);
Pattern pattern = Pattern.compile(REGEX_FOR_LOWERCASE_HEADERS);
Matcher matcher = pattern.matcher(headerName);

while (matcher.find()) {
String result = matcher.group();
headerName = headerName.replace(result, result.toUpperCase());
}

return headerName;
}

private static byte[] getResponseHeadBytes(int responseCode, BunqBasicHeader[] responseHeaders) {
String requestHeadString = responseCode + NEWLINE +
generateResponseHeadersSortedString(responseHeaders) + NEWLINE + NEWLINE;
Expand All @@ -565,8 +518,8 @@ private static String generateResponseHeadersSortedString(BunqBasicHeader[] resp
return Arrays.stream(responseHeaders)
.filter(
header ->
header.getName().startsWith(HEADER_NAME_PREFIX_X_BUNQ) &&
!header.getName().equals(HEADER_SERVER_SIGNATURE)
header.getName().isBunq() &&
!header.getName().equals(BunqHeader.serverSignature)
)
.map(header -> header.getName() + DELIMITER_HEADER_NAME_AND_VALUE + header.getValue())
.sorted()
Expand Down
Loading

0 comments on commit 4be80e5

Please sign in to comment.