Implement HMAC authentication for remote portrait store
This commit is contained in:
parent
39da801a61
commit
c52a25f2bc
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
Loading…
Reference in New Issue