Implement HMAC authentication for remote portrait store
This commit is contained in:
parent
39da801a61
commit
c52a25f2bc
|
@ -1,20 +1,23 @@
|
||||||
package ctbrec;
|
package ctbrec;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class Hmac {
|
public class Hmac {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(Hmac.class);
|
private Hmac() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Hmac.class);
|
||||||
|
|
||||||
public static byte[] generateKey() {
|
public static byte[] generateKey() {
|
||||||
LOG.debug("Generating HMAC key");
|
LOG.debug("Generating HMAC key");
|
||||||
|
@ -24,32 +27,40 @@ public class Hmac {
|
||||||
return Base64.getEncoder().encode(key);
|
return Base64.getEncoder().encode(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String calculate(String msg, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException, UnsupportedEncodingException {
|
public static String calculate(String msg, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException {
|
||||||
|
return calculate(msg.getBytes(UTF_8), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String calculate(byte[] msg, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException {
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
|
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
|
||||||
mac.init(keySpec);
|
mac.init(keySpec);
|
||||||
byte[] result = mac.doFinal(msg.getBytes("UTF-8"));
|
byte[] result = mac.doFinal(msg);
|
||||||
String hmac = bytesToHex(result);
|
String hmac = bytesToHex(result);
|
||||||
return hmac;
|
return hmac;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean validate(String msg, byte[] key, String hmacToCheck) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {
|
public static boolean validate(String msg, byte[] key, String hmacToCheck) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||||
|
return Hmac.calculate(msg, key).equals(hmacToCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validate(byte[] msg, byte[] key, String hmacToCheck) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||||
return Hmac.calculate(msg, key).equals(hmacToCheck);
|
return Hmac.calculate(msg, key).equals(hmacToCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a byte array to a string
|
* Converts a byte array to a string
|
||||||
*
|
*
|
||||||
* @param hash
|
* @param bytes the byte array to convert to a hex string
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static String bytesToHex(byte[] hash) {
|
public static String bytesToHex(byte[] bytes) {
|
||||||
if (hash == null) {
|
if (bytes == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
StringBuffer hexString = new StringBuffer();
|
StringBuilder hexString = new StringBuilder();
|
||||||
for (int i = 0; i < hash.length; i++) {
|
for (byte b : bytes) {
|
||||||
String hex = Integer.toHexString(0xff & hash[i]);
|
String hex = Integer.toHexString(0xff & b);
|
||||||
if (hex.length() == 1)
|
if (hex.length() == 1)
|
||||||
hexString.append('0');
|
hexString.append('0');
|
||||||
hexString.append(hex);
|
hexString.append(hex);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ctbrec.image;
|
package ctbrec.image;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Hmac;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
@ -12,6 +13,8 @@ import okhttp3.Response;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static ctbrec.io.HttpConstants.MIMETYPE_IMAGE_JPG;
|
import static ctbrec.io.HttpConstants.MIMETYPE_IMAGE_JPG;
|
||||||
|
@ -43,42 +46,61 @@ public class RemotePortraitStore implements PortraitStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<byte[]> load(String url) throws IOException {
|
private Optional<byte[]> load(String url) throws IOException {
|
||||||
Request req = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(url);
|
||||||
.url(url)
|
try {
|
||||||
.build();
|
addHmacIfNeeded(new byte[0], builder);
|
||||||
try (Response resp = httpClient.execute(req)) {
|
try (Response resp = httpClient.execute(builder.build())) {
|
||||||
if (resp.isSuccessful()) {
|
if (resp.isSuccessful()) {
|
||||||
return Optional.of(resp.body().bytes());
|
return Optional.of(resp.body().bytes());
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(resp.code(), resp.message());
|
throw new HttpException(resp.code(), resp.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||||
|
throw new IOException("Could not load portrait from server", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHmacIfNeeded(byte[] msg, Request.Builder builder) throws InvalidKeyException, NoSuchAlgorithmException {
|
||||||
|
if (Config.getInstance().getSettings().requireAuthentication) {
|
||||||
|
byte[] key = Config.getInstance().getSettings().key;
|
||||||
|
String hmac = Hmac.calculate(msg, key);
|
||||||
|
builder.addHeader("CTBREC-HMAC", hmac);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writePortrait(String modelUrl, byte[] data) throws IOException {
|
public void writePortrait(String modelUrl, byte[] data) throws IOException {
|
||||||
RequestBody body = RequestBody.create(data, MediaType.parse(MIMETYPE_IMAGE_JPG));
|
RequestBody body = RequestBody.create(data, MediaType.parse(MIMETYPE_IMAGE_JPG));
|
||||||
Request req = new Request.Builder()
|
Request.Builder builder = new Request.Builder()
|
||||||
.url(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8))
|
.url(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8))
|
||||||
.post(body)
|
.post(body);
|
||||||
.build();
|
try {
|
||||||
try (Response resp = httpClient.execute(req)) {
|
addHmacIfNeeded(data, builder);
|
||||||
|
try (Response resp = httpClient.execute(builder.build())) {
|
||||||
if (!resp.isSuccessful()) {
|
if (!resp.isSuccessful()) {
|
||||||
throw new HttpException(resp.code(), resp.message());
|
throw new HttpException(resp.code(), resp.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||||
|
throw new IOException("Could upload portrait to server", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removePortrait(String modelUrl) throws IOException {
|
public void removePortrait(String modelUrl) throws IOException {
|
||||||
Request req = new Request.Builder()
|
Request.Builder builder = new Request.Builder()
|
||||||
.url(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8))
|
.url(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8))
|
||||||
.delete()
|
.delete();
|
||||||
.build();
|
try {
|
||||||
try (Response resp = httpClient.execute(req)) {
|
addHmacIfNeeded(new byte[0], builder);
|
||||||
|
try (Response resp = httpClient.execute(builder.build())) {
|
||||||
if (!resp.isSuccessful()) {
|
if (!resp.isSuccessful()) {
|
||||||
throw new HttpException(resp.code(), resp.message());
|
throw new HttpException(resp.code(), resp.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||||
|
throw new IOException("Could not delete portrait from server", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,8 @@ public abstract class AbstractCtbrecServlet extends HttpServlet {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractCtbrecServlet.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractCtbrecServlet.class);
|
||||||
|
|
||||||
boolean checkAuthentication(HttpServletRequest req, String body) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
boolean checkAuthentication(HttpServletRequest req, String body) throws InvalidKeyException, NoSuchAlgorithmException {
|
||||||
boolean authenticated = false;
|
boolean authenticated;
|
||||||
if (Config.getInstance().getSettings().key != null) {
|
if (Config.getInstance().getSettings().key != null) {
|
||||||
String reqParamHmac = req.getParameter("hmac");
|
String reqParamHmac = req.getParameter("hmac");
|
||||||
String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
|
String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
|
||||||
|
@ -45,11 +45,39 @@ public abstract class AbstractCtbrecServlet extends HttpServlet {
|
||||||
return authenticated;
|
return authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean checkAuthentication(HttpServletRequest req, byte[] body) throws InvalidKeyException, NoSuchAlgorithmException {
|
||||||
|
boolean authenticated;
|
||||||
|
if (Config.getInstance().getSettings().key != null) {
|
||||||
|
String reqParamHmac = req.getParameter("hmac");
|
||||||
|
String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
|
||||||
|
String hmac = null;
|
||||||
|
String url = req.getRequestURI();
|
||||||
|
url = url.substring(getServletContext().getContextPath().length());
|
||||||
|
|
||||||
|
if (reqParamHmac != null) {
|
||||||
|
hmac = reqParamHmac;
|
||||||
|
}
|
||||||
|
if (httpHeaderHmac != null) {
|
||||||
|
hmac = httpHeaderHmac;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] key = Config.getInstance().getSettings().key;
|
||||||
|
if (reqParamHmac != null) {
|
||||||
|
authenticated = Hmac.validate(url, key, hmac);
|
||||||
|
} else {
|
||||||
|
authenticated = Hmac.validate(body, key, hmac);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authenticated = true;
|
||||||
|
}
|
||||||
|
return authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
String body(HttpServletRequest req) throws IOException {
|
String body(HttpServletRequest req) throws IOException {
|
||||||
StringBuilder body = new StringBuilder();
|
StringBuilder body = new StringBuilder();
|
||||||
BufferedReader br = req.getReader();
|
BufferedReader br = req.getReader();
|
||||||
String line = null;
|
String line;
|
||||||
while ((line = br.readLine()) != null) {
|
while ((line = br.readLine()) != null) {
|
||||||
body.append(line).append("\n");
|
body.append(line).append("\n");
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ public class ImageServlet extends AbstractCtbrecServlet {
|
||||||
|
|
||||||
public static final String BASE_URL = "/image";
|
public static final String BASE_URL = "/image";
|
||||||
public static final String INTERNAL_SERVER_ERROR = "Internal Server Error";
|
public static final String INTERNAL_SERVER_ERROR = "Internal Server Error";
|
||||||
|
private static final String HMAC_ERROR_DOCUMENT = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}";
|
||||||
private static final Pattern URL_PATTERN_PORTRAIT_BY_ID = Pattern.compile(BASE_URL + "/portrait/([0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})");
|
private static final Pattern URL_PATTERN_PORTRAIT_BY_ID = Pattern.compile(BASE_URL + "/portrait/([0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})");
|
||||||
private static final Pattern URL_PATTERN_PORTRAIT_BY_URL = Pattern.compile(BASE_URL + "/portrait/url/(.*)");
|
private static final Pattern URL_PATTERN_PORTRAIT_BY_URL = Pattern.compile(BASE_URL + "/portrait/url/(.*)");
|
||||||
private final PortraitStore portraitStore;
|
private final PortraitStore portraitStore;
|
||||||
|
@ -32,9 +33,9 @@ public class ImageServlet extends AbstractCtbrecServlet {
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
String requestURI = req.getRequestURI();
|
String requestURI = req.getRequestURI();
|
||||||
try {
|
try {
|
||||||
boolean authenticated = checkAuthentication(req, body(req));
|
boolean authenticated = checkAuthentication(req, "");
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match");
|
sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,16 +73,16 @@ public class ImageServlet extends AbstractCtbrecServlet {
|
||||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
String requestURI = req.getRequestURI();
|
String requestURI = req.getRequestURI();
|
||||||
try {
|
try {
|
||||||
// boolean authenticated = checkAuthentication(req, body(req));
|
byte[] data = bodyAsByteArray(req);
|
||||||
// if (!authenticated) {
|
boolean authenticated = checkAuthentication(req, data);
|
||||||
// sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match");
|
if (!authenticated) {
|
||||||
// return;
|
sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT);
|
||||||
// }
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Matcher m;
|
Matcher m;
|
||||||
if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
|
if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
|
||||||
String modelUrl = URLDecoder.decode(m.group(1), UTF_8);
|
String modelUrl = URLDecoder.decode(m.group(1), UTF_8);
|
||||||
byte[] data = bodyAsByteArray(req);
|
|
||||||
portraitStore.writePortrait(modelUrl, data);
|
portraitStore.writePortrait(modelUrl, data);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -94,11 +95,11 @@ public class ImageServlet extends AbstractCtbrecServlet {
|
||||||
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) {
|
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
String requestURI = req.getRequestURI();
|
String requestURI = req.getRequestURI();
|
||||||
try {
|
try {
|
||||||
// boolean authenticated = checkAuthentication(req, body(req));
|
boolean authenticated = checkAuthentication(req, "");
|
||||||
// if (!authenticated) {
|
if (!authenticated) {
|
||||||
// sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match");
|
sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
Matcher m;
|
Matcher m;
|
||||||
if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
|
if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
|
||||||
|
|
Loading…
Reference in New Issue