From c52a25f2bcc03c05b76e84c6ed35f03ecad46b34 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Fri, 21 Apr 2023 18:01:40 +0200 Subject: [PATCH] Implement HMAC authentication for remote portrait store --- common/src/main/java/ctbrec/Hmac.java | 43 ++++++++----- .../ctbrec/image/RemotePortraitStore.java | 62 +++++++++++++------ .../server/AbstractCtbrecServlet.java | 34 +++++++++- .../ctbrec/recorder/server/ImageServlet.java | 27 ++++---- 4 files changed, 114 insertions(+), 52 deletions(-) diff --git a/common/src/main/java/ctbrec/Hmac.java b/common/src/main/java/ctbrec/Hmac.java index f58370b6..d532f279 100644 --- a/common/src/main/java/ctbrec/Hmac.java +++ b/common/src/main/java/ctbrec/Hmac.java @@ -1,20 +1,23 @@ 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.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static java.nio.charset.StandardCharsets.UTF_8; 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() { LOG.debug("Generating HMAC key"); @@ -24,32 +27,40 @@ public class Hmac { 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"); SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256"); mac.init(keySpec); - byte[] result = mac.doFinal(msg.getBytes("UTF-8")); + byte[] result = mac.doFinal(msg); String hmac = bytesToHex(result); 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); } /** * Converts a byte array to a string * - * @param hash + * @param bytes the byte array to convert to a hex string * @return string */ - public static String bytesToHex(byte[] hash) { - if (hash == null) { + public static String bytesToHex(byte[] bytes) { + if (bytes == null) { return ""; } - StringBuffer hexString = new StringBuffer(); - for (int i = 0; i < hash.length; i++) { - String hex = Integer.toHexString(0xff & hash[i]); + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); diff --git a/common/src/main/java/ctbrec/image/RemotePortraitStore.java b/common/src/main/java/ctbrec/image/RemotePortraitStore.java index 9db28ae7..b762219c 100644 --- a/common/src/main/java/ctbrec/image/RemotePortraitStore.java +++ b/common/src/main/java/ctbrec/image/RemotePortraitStore.java @@ -1,6 +1,7 @@ package ctbrec.image; import ctbrec.Config; +import ctbrec.Hmac; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import lombok.RequiredArgsConstructor; @@ -12,6 +13,8 @@ import okhttp3.Response; import java.io.IOException; import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Optional; import static ctbrec.io.HttpConstants.MIMETYPE_IMAGE_JPG; @@ -43,42 +46,61 @@ public class RemotePortraitStore implements PortraitStore { } private Optional load(String url) throws IOException { - Request req = new Request.Builder() - .url(url) - .build(); - try (Response resp = httpClient.execute(req)) { - if (resp.isSuccessful()) { - return Optional.of(resp.body().bytes()); - } else { - throw new HttpException(resp.code(), resp.message()); + Request.Builder builder = new Request.Builder().url(url); + try { + addHmacIfNeeded(new byte[0], builder); + try (Response resp = httpClient.execute(builder.build())) { + if (resp.isSuccessful()) { + return Optional.of(resp.body().bytes()); + } else { + 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 public void writePortrait(String modelUrl, byte[] data) throws IOException { 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)) - .post(body) - .build(); - try (Response resp = httpClient.execute(req)) { - if (!resp.isSuccessful()) { - throw new HttpException(resp.code(), resp.message()); + .post(body); + try { + addHmacIfNeeded(data, builder); + try (Response resp = httpClient.execute(builder.build())) { + if (!resp.isSuccessful()) { + throw new HttpException(resp.code(), resp.message()); + } } + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IOException("Could upload portrait to server", e); } } @Override 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)) - .delete() - .build(); - try (Response resp = httpClient.execute(req)) { - if (!resp.isSuccessful()) { - throw new HttpException(resp.code(), resp.message()); + .delete(); + try { + addHmacIfNeeded(new byte[0], builder); + try (Response resp = httpClient.execute(builder.build())) { + if (!resp.isSuccessful()) { + throw new HttpException(resp.code(), resp.message()); + } } + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IOException("Could not delete portrait from server", e); } } } diff --git a/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java b/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java index 13db9f6b..3c2d157d 100644 --- a/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java @@ -20,8 +20,8 @@ public abstract class AbstractCtbrecServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(AbstractCtbrecServlet.class); - boolean checkAuthentication(HttpServletRequest req, String body) throws IOException, InvalidKeyException, NoSuchAlgorithmException { - boolean authenticated = false; + boolean checkAuthentication(HttpServletRequest req, String body) throws InvalidKeyException, NoSuchAlgorithmException { + boolean authenticated; if (Config.getInstance().getSettings().key != null) { String reqParamHmac = req.getParameter("hmac"); String httpHeaderHmac = req.getHeader("CTBREC-HMAC"); @@ -45,11 +45,39 @@ public abstract class AbstractCtbrecServlet extends HttpServlet { 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 { StringBuilder body = new StringBuilder(); BufferedReader br = req.getReader(); - String line = null; + String line; while ((line = br.readLine()) != null) { body.append(line).append("\n"); } diff --git a/server/src/main/java/ctbrec/recorder/server/ImageServlet.java b/server/src/main/java/ctbrec/recorder/server/ImageServlet.java index efb0792c..bfac2fc3 100644 --- a/server/src/main/java/ctbrec/recorder/server/ImageServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/ImageServlet.java @@ -23,6 +23,7 @@ public class ImageServlet extends AbstractCtbrecServlet { public static final String BASE_URL = "/image"; 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_URL = Pattern.compile(BASE_URL + "/portrait/url/(.*)"); private final PortraitStore portraitStore; @@ -32,9 +33,9 @@ public class ImageServlet extends AbstractCtbrecServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) { String requestURI = req.getRequestURI(); try { - boolean authenticated = checkAuthentication(req, body(req)); + boolean authenticated = checkAuthentication(req, ""); if (!authenticated) { - sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match"); + sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); return; } @@ -72,16 +73,16 @@ public class ImageServlet extends AbstractCtbrecServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) { String requestURI = req.getRequestURI(); try { - // boolean authenticated = checkAuthentication(req, body(req)); - // if (!authenticated) { - // sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match"); - // return; - // } + byte[] data = bodyAsByteArray(req); + boolean authenticated = checkAuthentication(req, data); + if (!authenticated) { + sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); + return; + } Matcher m; if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) { String modelUrl = URLDecoder.decode(m.group(1), UTF_8); - byte[] data = bodyAsByteArray(req); portraitStore.writePortrait(modelUrl, data); } } catch (Exception e) { @@ -94,11 +95,11 @@ public class ImageServlet extends AbstractCtbrecServlet { protected void doDelete(HttpServletRequest req, HttpServletResponse resp) { String requestURI = req.getRequestURI(); try { - // boolean authenticated = checkAuthentication(req, body(req)); - // if (!authenticated) { - // sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match"); - // return; - // } + boolean authenticated = checkAuthentication(req, ""); + if (!authenticated) { + sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); + return; + } Matcher m; if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {