forked from j62/ctbrec
1
0
Fork 0

Implement HMAC authentication for remote portrait store

This commit is contained in:
0xb00bface 2023-04-21 18:01:40 +02:00
parent 39da801a61
commit c52a25f2bc
4 changed files with 114 additions and 52 deletions

View File

@ -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);

View File

@ -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);
if (!resp.isSuccessful()) { try (Response resp = httpClient.execute(builder.build())) {
throw new HttpException(resp.code(), resp.message()); if (!resp.isSuccessful()) {
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);
if (!resp.isSuccessful()) { try (Response resp = httpClient.execute(builder.build())) {
throw new HttpException(resp.code(), resp.message()); if (!resp.isSuccessful()) {
throw new HttpException(resp.code(), resp.message());
}
} }
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new IOException("Could not delete portrait from server", e);
} }
} }
} }

View File

@ -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");
} }

View File

@ -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()) {