From d62bba559949a379da9066425faf881f937d7818 Mon Sep 17 00:00:00 2001
From: 0xboobface <0xboobface@gmail.com>
Date: Sat, 7 Jul 2018 12:50:21 +0200
Subject: [PATCH] Implemented HMAC authentication in RecorderServlet

---
 src/main/java/ctbrec/Config.java              |  2 +-
 src/main/java/ctbrec/Hmac.java                | 55 +++++++++++++++++++
 src/main/java/ctbrec/Settings.java            |  2 +
 .../recorder/server/RecorderServlet.java      | 28 ++++++++++
 4 files changed, 86 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/ctbrec/Hmac.java

diff --git a/src/main/java/ctbrec/Config.java b/src/main/java/ctbrec/Config.java
index 5c9db23d..a539ac0d 100644
--- a/src/main/java/ctbrec/Config.java
+++ b/src/main/java/ctbrec/Config.java
@@ -71,7 +71,7 @@ public class Config {
 
     public void save() throws IOException {
         Moshi moshi = new Moshi.Builder().build();
-        JsonAdapter<Settings> adapter = moshi.adapter(Settings.class);
+        JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent("  ");
         String json = adapter.toJson(settings);
         File configDir = OS.getConfigDir();
         File configFile = new File(configDir, filename);
diff --git a/src/main/java/ctbrec/Hmac.java b/src/main/java/ctbrec/Hmac.java
new file mode 100644
index 00000000..46f1751f
--- /dev/null
+++ b/src/main/java/ctbrec/Hmac.java
@@ -0,0 +1,55 @@
+package ctbrec;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Hmac {
+
+    private static final transient Logger LOG = LoggerFactory.getLogger(Hmac.class);
+
+    public static byte[] generateKey() {
+        LOG.debug("Generating key");
+        SecureRandom random = new SecureRandom();
+        byte[] key = new byte[32];
+        random.nextBytes(key);
+        return key;
+    }
+
+    public static String calculate(String msg, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException, UnsupportedEncodingException {
+        Mac mac = Mac.getInstance("HmacSHA256");
+        SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
+        mac.init(keySpec);
+        byte[] result = mac.doFinal(msg.getBytes("UTF-8"));
+        String hmac = bytesToHex(result);
+        return hmac;
+    }
+
+    public static boolean validate(String msg, byte[] key, String hmacToCheck) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {
+        return Hmac.calculate(msg, key).equals(hmacToCheck);
+    }
+
+    /**
+     * Converts a byte array to a string
+     *
+     * @param hash
+     * @return string
+     */
+    static String bytesToHex(byte[] hash) {
+        StringBuffer hexString = new StringBuffer();
+        for (int i = 0; i < hash.length; i++) {
+            String hex = Integer.toHexString(0xff & hash[i]);
+            if (hex.length() == 1)
+                hexString.append('0');
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
+}
diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java
index 4d55d2b4..3d18cee8 100644
--- a/src/main/java/ctbrec/Settings.java
+++ b/src/main/java/ctbrec/Settings.java
@@ -16,4 +16,6 @@ public class Settings {
     public String lastDownloadDir = "";
     public List<Model> models = new ArrayList<Model>();
     public boolean determineResolution = false;
+    public boolean requireAuthentication = false;
+    public byte[] key = null;
 }
diff --git a/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/src/main/java/ctbrec/recorder/server/RecorderServlet.java
index c0d8d549..a660b7b1 100644
--- a/src/main/java/ctbrec/recorder/server/RecorderServlet.java
+++ b/src/main/java/ctbrec/recorder/server/RecorderServlet.java
@@ -3,9 +3,12 @@ package ctbrec.recorder.server;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Iterator;
 import java.util.List;
@@ -21,6 +24,8 @@ import org.slf4j.LoggerFactory;
 import com.squareup.moshi.JsonAdapter;
 import com.squareup.moshi.Moshi;
 
+import ctbrec.Config;
+import ctbrec.Hmac;
 import ctbrec.InstantJsonAdapter;
 import ctbrec.Model;
 import ctbrec.Recording;
@@ -43,6 +48,14 @@ public class RecorderServlet extends HttpServlet {
 
         try {
             String json = body(req);
+            boolean isRequestAuthenticated = checkAuthentication(req, json);
+            if(!isRequestAuthenticated) {
+                resp.setStatus(SC_UNAUTHORIZED);
+                String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}";
+                resp.getWriter().write(response);
+                return;
+            }
+
             LOG.debug("Request: {}", json);
             Moshi moshi = new Moshi.Builder()
                     .add(Instant.class, new InstantJsonAdapter())
@@ -116,6 +129,21 @@ public class RecorderServlet extends HttpServlet {
         }
     }
 
+    private boolean checkAuthentication(HttpServletRequest req, String body) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
+        boolean authenticated = false;
+        if(Config.getInstance().getSettings().key != null) {
+            if(req.getHeader("CTBREC-HMAC") == null) {
+                authenticated = false;
+            }
+
+            byte[] key = Config.getInstance().getSettings().key;
+            authenticated = Hmac.validate(body, key, req.getHeader("CTBREC-HMAC"));
+        } else {
+            authenticated = true;
+        }
+        return authenticated;
+    }
+
     private String body(HttpServletRequest req) throws IOException {
         StringBuilder body = new StringBuilder();
         BufferedReader br = req.getReader();