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

View File

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

View File

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

View File

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