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<byte[]> 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()) {