| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The SF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations under the License. |
| */ |
| package org.apache.sling.discovery.base.connectors.ping; |
| |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.security.AlgorithmParameters; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.Key; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.security.spec.InvalidKeySpecException; |
| import java.security.spec.InvalidParameterSpecException; |
| import java.security.spec.KeySpec; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.zip.GZIPInputStream; |
| |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.Mac; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.SecretKey; |
| import javax.crypto.SecretKeyFactory; |
| import javax.crypto.spec.IvParameterSpec; |
| import javax.crypto.spec.PBEKeySpec; |
| import javax.crypto.spec.SecretKeySpec; |
| import javax.json.Json; |
| import javax.json.JsonArray; |
| import javax.json.JsonArrayBuilder; |
| import javax.json.JsonException; |
| import javax.json.JsonObject; |
| import javax.json.JsonObjectBuilder; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.commons.codec.binary.Base64; |
| import org.apache.commons.io.IOUtils; |
| import org.apache.http.Header; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.client.methods.HttpUriRequest; |
| import org.apache.sling.discovery.base.connectors.BaseConfig; |
| |
| /** |
| * Request Validator helper. |
| */ |
| public class TopologyRequestValidator { |
| |
| public static final String SIG_HEADER = "X-SlingTopologyTrust"; |
| |
| public static final String HASH_HEADER = "X-SlingTopologyHash"; |
| |
| /** |
| * Maximum number of keys to keep in memory. |
| */ |
| private static final int MAXKEYS = 5; |
| |
| /** |
| * Minimum number of keys to keep in memory. |
| */ |
| private static final int MINKEYS = 3; |
| |
| /** |
| * true if trust information should be in request headers. |
| */ |
| private boolean trustEnabled; |
| |
| /** |
| * true if encryption of the message payload should be encrypted. |
| */ |
| private boolean encryptionEnabled; |
| |
| /** |
| * map of hmac keys, keyed by key number. |
| */ |
| private Map<Integer, Key> keys = new ConcurrentHashMap<Integer, Key>(); |
| |
| /** |
| * The shared key. |
| */ |
| private String sharedKey; |
| |
| /** |
| * TTL of each shared key generation. |
| */ |
| private long interval; |
| |
| /** |
| * If true, everything is deactivated. |
| */ |
| private boolean deactivated; |
| |
| private SecureRandom random = new SecureRandom(); |
| |
| /** |
| * Create a TopologyRequestValidator. |
| * |
| * @param config the configuation object |
| */ |
| public TopologyRequestValidator(BaseConfig config) { |
| trustEnabled = false; |
| encryptionEnabled = false; |
| if (config.isHmacEnabled()) { |
| trustEnabled = true; |
| sharedKey = config.getSharedKey(); |
| interval = config.getKeyInterval(); |
| encryptionEnabled = config.isEncryptionEnabled(); |
| } |
| deactivated = false; |
| } |
| |
| /** |
| * Encodes a request returning the encoded body |
| * |
| * @param body |
| * @return the encoded body. |
| * @throws IOException |
| */ |
| public String encodeMessage(String body) throws IOException { |
| checkActive(); |
| if (encryptionEnabled) { |
| try { |
| JsonObjectBuilder json = Json.createObjectBuilder(); |
| JsonArrayBuilder array = Json.createArrayBuilder(); |
| for (String value : encrypt(body)) |
| { |
| array.add(value); |
| } |
| json.add("payload", array); |
| StringWriter writer = new StringWriter(); |
| Json.createGenerator(writer).write(json.build()).close(); |
| return writer.toString(); |
| } catch (InvalidKeyException e) { |
| e.printStackTrace(); |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (IllegalBlockSizeException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (BadPaddingException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (UnsupportedEncodingException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (NoSuchAlgorithmException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (NoSuchPaddingException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (JsonException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (InvalidKeySpecException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } catch (InvalidParameterSpecException e) { |
| throw new IOException("Unable to Encrypt Message " + e.getMessage()); |
| } |
| |
| } |
| return body; |
| } |
| |
| /** |
| * Decode a message sent from the client. |
| * |
| * @param request the request object for the message. |
| * @return the message in clear text. |
| * @throws IOException if there is a problem decoding the message or the |
| * message is invalid. |
| */ |
| public String decodeMessage(HttpServletRequest request) throws IOException { |
| checkActive(); |
| return decodeMessage("request:", request.getRequestURI(), getRequestBody(request), |
| request.getHeader(HASH_HEADER)); |
| } |
| |
| /** |
| * Decode a response from the server. |
| * |
| * @param response the response. |
| * @return the message in clear text. |
| * @throws IOException if there was a problem decoding the message. |
| */ |
| public String decodeMessage(String uri, HttpResponse response) throws IOException { |
| checkActive(); |
| return decodeMessage("response:", uri, getResponseBody(response), |
| getResponseHeader(response, HASH_HEADER)); |
| } |
| |
| /** |
| * Decode a message |
| * |
| * @param prefix the prefix to indicate if the message is a request or |
| * response message. |
| * @param url the url associated with the message. |
| * @param body the body of the message. |
| * @param requestHash a hash of the message. |
| * @return the message in clear text |
| * @throws IOException if the message can't be decrypted. |
| */ |
| private String decodeMessage(String prefix, String url, String body, String requestHash) |
| throws IOException { |
| if (trustEnabled) { |
| String bodyHash = hash(prefix + url + ":" + body); |
| if (bodyHash.equals(requestHash)) { |
| if (encryptionEnabled) { |
| try { |
| JsonObject json = Json.createReader(new StringReader(body)).readObject(); |
| if (json.containsKey("payload")) { |
| return decrypt(json.getJsonArray("payload")); |
| } |
| } catch (JsonException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (InvalidKeyException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (IllegalBlockSizeException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (BadPaddingException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (NoSuchAlgorithmException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (NoSuchPaddingException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (InvalidAlgorithmParameterException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } catch (InvalidKeySpecException e) { |
| throw new IOException("Encrypted Message is in the correct json format"); |
| } |
| |
| } |
| } |
| throw new IOException("Message is not valid, hash does not match message"); |
| } |
| return body; |
| } |
| |
| /** |
| * Is the request from the client trusted, based on the signature headers. |
| * |
| * @param request the request. |
| * @return true if trusted, or true if this component is disabled. |
| */ |
| public boolean isTrusted(HttpServletRequest request) { |
| checkActive(); |
| if (trustEnabled) { |
| return checkTrustHeader(request.getHeader(HASH_HEADER), |
| request.getHeader(SIG_HEADER)); |
| } |
| return false; |
| } |
| |
| /** |
| * Is the response from the server to be trusted by the client. |
| * |
| * @param response the response |
| * @return true if trusted, or true if this component is disabled. |
| */ |
| public boolean isTrusted(HttpResponse response) { |
| checkActive(); |
| if (trustEnabled) { |
| return checkTrustHeader(getResponseHeader(response, HASH_HEADER), |
| getResponseHeader(response, SIG_HEADER)); |
| } |
| return false; |
| } |
| |
| /** |
| * Trust a message on the client before sending, only if trust is enabled. |
| * |
| * @param method the method which will have headers set after the call. |
| * @param body the body. |
| */ |
| public void trustMessage(HttpUriRequest method, String body) { |
| checkActive(); |
| if (trustEnabled) { |
| String bodyHash = hash("request:" + method.getURI().getPath() + ":" + body); |
| method.setHeader(HASH_HEADER, bodyHash); |
| method.setHeader(SIG_HEADER, createTrustHeader(bodyHash)); |
| } |
| } |
| |
| /** |
| * Trust a response message sent from the server to the client. |
| * |
| * @param response the response. |
| * @param request the request, |
| * @param body body of the response. |
| */ |
| public void trustMessage(HttpServletResponse response, HttpServletRequest request, String body) { |
| checkActive(); |
| if (trustEnabled) { |
| String bodyHash = hash("response:" + request.getRequestURI() + ":" + body); |
| response.setHeader(HASH_HEADER, bodyHash); |
| response.setHeader(SIG_HEADER, createTrustHeader(bodyHash)); |
| } |
| } |
| |
| /** |
| * @param body |
| * @return a hash of body base64 encoded. |
| */ |
| private String hash(String toHash) { |
| try { |
| MessageDigest m = MessageDigest.getInstance("SHA-256"); |
| return new String(Base64.encodeBase64(m.digest(toHash.getBytes("UTF-8"))), "UTF-8"); |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Generate a signature of the bodyHash and encode it so that it contains |
| * the key number used to generate the signature. |
| * |
| * @param bodyHash a hash |
| * @return the signature. |
| */ |
| private String createTrustHeader(String bodyHash) { |
| try { |
| int keyNo = getCurrentKey(); |
| return keyNo + "/" + hmac(keyNo, bodyHash); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (InvalidKeyException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (IllegalStateException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Check that the signature is a signature of the body hash. |
| * |
| * @param bodyHash the body hash. |
| * @param signature the signature. |
| * @return true if the signature can be trusted. |
| */ |
| private boolean checkTrustHeader(String bodyHash, String signature) { |
| try { |
| if (bodyHash == null || signature == null ) { |
| return false; |
| } |
| String[] parts = signature.split("/", 2); |
| int keyNo = Integer.parseInt(parts[0]); |
| return hmac(keyNo, bodyHash).equals(parts[1]); |
| } catch (ArrayIndexOutOfBoundsException e) { |
| return false; |
| } catch (IllegalArgumentException e) { |
| return false; |
| } catch (InvalidKeyException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (IllegalStateException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } catch (Exception e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Get a Mac instance for the key number. |
| * |
| * @param keyNo the key number. |
| * @return the mac instance. |
| * @throws NoSuchAlgorithmException |
| * @throws InvalidKeyException |
| * @throws UnsupportedEncodingException |
| */ |
| private Mac getMac(int keyNo) throws NoSuchAlgorithmException, InvalidKeyException, |
| UnsupportedEncodingException { |
| Mac m = Mac.getInstance("HmacSHA256"); |
| m.init(getKey(keyNo)); |
| return m; |
| } |
| |
| /** |
| * Perform a HMAC on the body using the key specified. |
| * |
| * @param keyNo the key number. |
| * @param bodyHash a hash of the body. |
| * @return the hmac signature. |
| * @throws InvalidKeyException |
| * @throws UnsupportedEncodingException |
| * @throws IllegalStateException |
| * @throws NoSuchAlgorithmException |
| */ |
| private String hmac(int keyNo, String bodyHash) throws InvalidKeyException, |
| UnsupportedEncodingException, IllegalStateException, NoSuchAlgorithmException { |
| return new String(Base64.encodeBase64(getMac(keyNo).doFinal(bodyHash.getBytes("UTF-8"))), |
| "UTF-8"); |
| } |
| |
| /** |
| * Decrypt the body. |
| * |
| * @param jsonArray the encrypted payload |
| * @return the decrypted payload. |
| * @throws IllegalBlockSizeException |
| * @throws BadPaddingException |
| * @throws UnsupportedEncodingException |
| * @throws InvalidKeyException |
| * @throws NoSuchAlgorithmException |
| * @throws NoSuchPaddingException |
| * @throws InvalidKeySpecException |
| * @throws InvalidAlgorithmParameterException |
| * @throws JSONException |
| */ |
| private String decrypt(JsonArray jsonArray) throws IllegalBlockSizeException, |
| BadPaddingException, UnsupportedEncodingException, InvalidKeyException, |
| NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException { |
| Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
| cipher.init(Cipher.DECRYPT_MODE, getCiperKey(Base64.decodeBase64(jsonArray.get(0).toString().getBytes("UTF-8"))), new IvParameterSpec(Base64.decodeBase64(jsonArray.get(1).toString().getBytes("UTF-8")))); |
| return new String(cipher.doFinal(Base64.decodeBase64(jsonArray.get(2).toString().getBytes("UTF-8")))); |
| } |
| |
| /** |
| * Encrypt a payload with the numbed key/ |
| * |
| * @param payload the payload. |
| * @param keyNo the key number. |
| * @return an encrypted version. |
| * @throws IllegalBlockSizeException |
| * @throws BadPaddingException |
| * @throws UnsupportedEncodingException |
| * @throws InvalidKeyException |
| * @throws NoSuchAlgorithmException |
| * @throws NoSuchPaddingException |
| * @throws InvalidKeySpecException |
| * @throws InvalidParameterSpecException |
| */ |
| private List<String> encrypt(String payload) throws IllegalBlockSizeException, |
| BadPaddingException, UnsupportedEncodingException, InvalidKeyException, |
| NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidParameterSpecException { |
| Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
| byte[] salt = new byte[9]; |
| random.nextBytes(salt); |
| cipher.init(Cipher.ENCRYPT_MODE, getCiperKey(salt)); |
| AlgorithmParameters params = cipher.getParameters(); |
| List<String> encrypted = new ArrayList<String>(); |
| encrypted.add(new String(Base64.encodeBase64(salt))); |
| encrypted.add(new String(Base64.encodeBase64(params.getParameterSpec(IvParameterSpec.class).getIV()))); |
| encrypted.add(new String(Base64.encodeBase64(cipher.doFinal(payload.getBytes("UTF-8"))))); |
| return encrypted; |
| } |
| |
| /** |
| * @param salt number of the key. |
| * @return the CupherKey. |
| * @throws UnsupportedEncodingException |
| * @throws NoSuchAlgorithmException |
| * @throws InvalidKeySpecException |
| */ |
| private Key getCiperKey(byte[] salt) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException { |
| SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); |
| // hashing the password 65K times takes 151ms, hashing 256 times takes 2ms. |
| // Since the salt has 2^^72 values, 256 times is probably good enough. |
| KeySpec spec = new PBEKeySpec(sharedKey.toCharArray(), salt, 256, 128); |
| SecretKey tmp = factory.generateSecret(spec); |
| SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); |
| return key; |
| } |
| |
| /** |
| * @param keyNo number of the key. |
| * @return the HMac key. |
| * @throws UnsupportedEncodingException |
| */ |
| private Key getKey(int keyNo) throws UnsupportedEncodingException { |
| if(Math.abs(keyNo - getCurrentKey()) > 1 ) { |
| throw new IllegalArgumentException("Key has expired"); |
| } |
| if (keys.containsKey(keyNo)) { |
| return keys.get(keyNo); |
| } |
| trimKeys(); |
| SecretKeySpec key = new SecretKeySpec(hash(sharedKey + keyNo).getBytes("UTF-8"), |
| "HmacSHA256"); |
| keys.put(keyNo, key); |
| return key; |
| } |
| |
| private int getCurrentKey() { |
| return (int) (System.currentTimeMillis() / interval); |
| } |
| |
| /** |
| * dump olf keys. |
| */ |
| private void trimKeys() { |
| if (keys.size() > MAXKEYS) { |
| List<Integer> keysKeys = new ArrayList<Integer>(keys.keySet()); |
| Collections.sort(keysKeys); |
| for (Integer k : keysKeys) { |
| if (keys.size() < MINKEYS) { |
| break; |
| } |
| keys.remove(k); |
| } |
| } |
| |
| } |
| |
| /** |
| * Get the value of a response header. |
| * |
| * @param response the response |
| * @param name the name of the response header. |
| * @return the value of the response header, null if none. |
| */ |
| private String getResponseHeader(HttpResponse response, String name) { |
| Header h = response.getFirstHeader(name); |
| if (h == null) { |
| return null; |
| } |
| return h.getValue(); |
| } |
| |
| /** |
| * Get the request body. |
| * |
| * @param request the request. |
| * @return the body as a string. |
| * @throws IOException |
| */ |
| private String getRequestBody(HttpServletRequest request) throws IOException { |
| final String contentEncoding = request.getHeader("Content-Encoding"); |
| if (contentEncoding!=null && contentEncoding.contains("gzip")) { |
| // then treat the request body as gzip: |
| final GZIPInputStream gzipIn = new GZIPInputStream(request.getInputStream()); |
| final String gunzippedEncodedJson = IOUtils.toString(gzipIn); |
| gzipIn.close(); |
| return gunzippedEncodedJson; |
| } else { |
| // otherwise assume plain-text: |
| return IOUtils.toString(request.getReader()); |
| } |
| } |
| |
| /** |
| * @param response the response |
| * @return the body of the response from the server. |
| * @throws IOException |
| */ |
| private String getResponseBody(HttpResponse response) throws IOException { |
| final Header contentEncoding = response.getFirstHeader("Content-Encoding"); |
| if (contentEncoding!=null && contentEncoding.getValue()!=null && |
| contentEncoding.getValue().contains("gzip")) { |
| // then the server sent gzip - treat it so: |
| final GZIPInputStream gzipIn = new GZIPInputStream(response.getEntity().getContent()); |
| final String gunzippedEncodedJson = IOUtils.toString(gzipIn); |
| gzipIn.close(); |
| return gunzippedEncodedJson; |
| } else { |
| // otherwise the server sent plaintext: |
| return IOUtils.toString(response.getEntity().getContent(), "UTF-8"); |
| } |
| } |
| |
| /** |
| * throw an exception if not active. |
| */ |
| private void checkActive() { |
| if (deactivated) { |
| throw new IllegalStateException(this.getClass().getName() + " is not active"); |
| } |
| if ((trustEnabled || encryptionEnabled) && sharedKey == null) { |
| throw new IllegalStateException(this.getClass().getName() |
| + " Shared Key must be set if encryption or signing is enabled."); |
| } |
| } |
| |
| } |