blob: 62823fa341c65c7596b702b390bc4a3274bf9b0d [file] [log] [blame]
/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* Licensed 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.
* #L%
*/
package test.frames;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Instantiate;
import org.apache.felix.ipojo.annotations.Provides;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
/**
* This component triggers some interesting frames issue.
*/
@Component
@Provides
@Instantiate(name = "crypto")
public class CryptoServiceSingleton {
public static final String AES_CBC_ALGORITHM = "AES/CBC/PKCS5Padding";
public static final String AES_ECB_ALGORITHM = "AES";
private static final Charset UTF_8 = Charset.defaultCharset();
public static final String HMAC_SHA_1 = "HmacSHA1";
public static final String PBKDF_2_WITH_HMAC_SHA_1 = "PBKDF2WithHmacSHA1";
private int keySize;
private int iterationCount;
private Hash defaultHash;
private final String secret;
public CryptoServiceSingleton(String secret, Hash defaultHash,
Integer keySize, Integer iterationCount) {
this.secret = secret;
this.defaultHash = defaultHash;
this.keySize = keySize;
this.iterationCount = iterationCount;
}
public CryptoServiceSingleton() {
this(
"I;>qOs/VgFe?l@>Kn/RGa0p9b1ji?Kg7uhjAPHdIO8>@<em_AFs[BAMUQ0D]eOLV",
Hash.valueOf("MD5"),
128,
20);
}
/**
* Generate the AES key from the salt and the private key.
*
* @param salt the salt (hexadecimal)
* @param privateKey the private key
* @return the generated key.
*/
private SecretKey generateAESKey(String privateKey, String salt) {
try {
byte[] raw = Hex.decodeHex(salt.toCharArray());
KeySpec spec = new PBEKeySpec(privateKey.toCharArray(), raw, iterationCount, keySize);
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_1);
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), AES_ECB_ALGORITHM);
} catch (DecoderException e) {
throw new IllegalStateException(e);
} catch ( NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
} catch (InvalidKeySpecException e) {
throw new IllegalStateException(e);
}
}
/**
* Encrypt a String with the AES encryption advanced using 'AES/CBC/PKCS5Padding'. Unlike the regular
* encode/decode AES method using ECB (Electronic Codebook), it uses Cipher-block chaining (CBC). The salt must be
* valid hexadecimal String. This method uses parts of the application secret as private key and initialization
* vector.
*
* @param value The message to encrypt
* @param salt The salt (hexadecimal String)
* @return encrypted String encoded using Base64
*/
public String encryptAESWithCBC(String value, String salt) {
return encryptAESWithCBC(value, getSecretPrefix(), salt, getDefaultIV());
}
/**
* Encrypt a String with the AES encryption advanced using 'AES/CBC/PKCS5Padding'. Unlike the regular
* encode/decode AES method using ECB (Electronic Codebook), it uses Cipher-block chaining (CBC). The private key
* must have a length of 16 bytes, the salt and initialization vector must be valid hex Strings.
*
* @param value The message to encrypt
* @param privateKey The private key
* @param salt The salt (hexadecimal String)
* @param iv The initialization vector (hexadecimal String)
* @return encrypted String encoded using Base64
*/
public String encryptAESWithCBC(String value, String privateKey, String salt, String iv) {
SecretKey genKey = generateAESKey(privateKey, salt);
byte[] encrypted = doFinal(Cipher.ENCRYPT_MODE, genKey, iv, value.getBytes(UTF_8));
return new String(Base64.encodeBase64(encrypted), UTF_8);
}
/**
* Decrypt a String with the AES encryption advanced using 'AES/CBC/PKCS5Padding'. Unlike the regular
* encode/decode AES method using ECB (Electronic Codebook), it uses Cipher-block chaining (CBC). The salt and
* initialization vector must be valid hex Strings. This method use parts of the application secret as private
* key and the default initialization vector.
*
* @param value An encrypted String encoded using Base64.
* @param salt The salt (hexadecimal String)
* @return The decrypted String
*/
public String decryptAESWithCBC(String value, String salt) {
return decryptAESWithCBC(value, getSecretPrefix(), salt, getDefaultIV());
}
/**
* Decrypt a String with the AES encryption advanced using 'AES/CBC/PKCS5Padding'. Unlike the regular
* encode/decode AES method using ECB (Electronic Codebook), it uses Cipher-block chaining (CBC). The private key
* must have a length of 16 bytes, the salt and initialization vector must be valid hexadecimal Strings.
*
* @param value An encrypted String encoded using Base64.
* @param privateKey The private key
* @param salt The salt (hexadecimal String)
* @param iv The initialization vector (hexadecimal String)
* @return The decrypted String
*/
public String decryptAESWithCBC(String value, String privateKey, String salt, String iv) {
SecretKey key = generateAESKey(privateKey, salt);
byte[] decrypted = doFinal(Cipher.DECRYPT_MODE, key, iv, decodeBase64(value));
return new String(decrypted, UTF_8);
}
/**
* Utility method encrypting/decrypting the given message.
* The sense of the operation is specified using the `encryptMode` parameter.
*
* @param encryptMode encrypt or decrypt mode ({@link javax.crypto.Cipher#DECRYPT_MODE} or
* {@link javax.crypto.Cipher#ENCRYPT_MODE}).
* @param generatedKey the generated key
* @param vector the initialization vector
* @param message the plain/cipher text to encrypt/decrypt
* @return the encrypted or decrypted message
*/
private byte[] doFinal(int encryptMode, SecretKey generatedKey, String vector, byte[] message) {
try {
byte[] raw = Hex.decodeHex(vector.toCharArray());
Cipher cipher = Cipher.getInstance(AES_CBC_ALGORITHM);
cipher.init(encryptMode, generatedKey, new IvParameterSpec(raw));
return cipher.doFinal(message);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Sign a message using the application secret key (HMAC-SHA1).
*/
public String sign(String message) {
return sign(message, secret.getBytes(UTF_8));
}
/**
* Sign a message with a key.
*
* @param message The message to sign
* @param key The key to use
* @return The signed message (in hexadecimal)
*/
public String sign(String message, byte[] key) {
try {
// Get an hmac_sha1 key from the raw key bytes
SecretKeySpec signingKey = new SecretKeySpec(key, HMAC_SHA_1);
// Get an hmac_sha1 Mac instance and initialize with the signing key
Mac mac = Mac.getInstance(HMAC_SHA_1);
mac.init(signingKey);
// Compute the hmac on input data bytes
byte[] rawHmac = mac.doFinal(message.getBytes(UTF_8));
// Convert raw bytes to Hex
byte[] hexBytes = new Hex().encode(rawHmac);
// Covert array of Hex bytes to a String
return new String(hexBytes, UTF_8);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
/**
* Create a hash using the default hashing algorithm.
*
* @param input The password
* @return The password hash
*/
public String hash(String input) {
return hash(input, defaultHash);
}
/**
* Create a hash using specific hashing algorithm.
*
* @param input The password
* @param hashType The hashing algorithm
* @return The password hash
*/
public String hash(String input, Hash hashType) {
try {
MessageDigest m = MessageDigest.getInstance(hashType.toString());
byte[] out = m.digest(input.getBytes(UTF_8));
return new String(Base64.encodeBase64(out), UTF_8);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Encrypt a String with the AES standard encryption (using the ECB mode) using the default secret (the
* application secret).
*
* @param value The String to encrypt
* @return An hexadecimal encrypted string
*/
public String encryptAES(String value) {
return encryptAES(value, getSecretPrefix());
}
/**
* Encrypt a String with the AES standard encryption (using the ECB mode). Private key must have a length of 16 bytes.
*
* @param value The String to encrypt
* @param privateKey The key used to encrypt
* @return An hexadecimal encrypted string
*/
public String encryptAES(String value, String privateKey) {
try {
byte[] raw = privateKey.getBytes(UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES_ECB_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ECB_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
return Hex.encodeHexString(cipher.doFinal(value.getBytes(UTF_8)));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Decrypt a String with the standard AES encryption (using the ECB mode) using the default secret (the
* application secret).
*
* @param value An hexadecimal encrypted string
* @return The decrypted String
*/
public String decryptAES(String value) {
return decryptAES(value, getSecretPrefix());
}
/**
* Decrypt a String with the standard AES encryption (using the ECB mode). Private key must have a length of 16
* bytes.
*
* @param value An hexadecimal encrypted string
* @param privateKey The key used to encrypt
* @return The decrypted String
*/
public String decryptAES(String value, String privateKey) {
try {
byte[] raw = privateKey.getBytes(UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES_ECB_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ECB_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
return new String(cipher.doFinal(Hex.decodeHex(value.toCharArray())), UTF_8);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Gets the 16 first characters of the application secret.
*
* @return the secret prefix.
*/
private String getSecretPrefix() {
return secret.substring(0, 16);
}
/**
* Gets a segment of the application secret of 16 characters and encoded them in hexadecimal. The segment
* contains from the 16th to the 32th characters from the application secret (16 characters). The extracted
* segment is encoded in hexadecimal
*
* @return the default initialization vector.
*/
private String getDefaultIV() {
return String.valueOf(Hex.encodeHex(secret.substring(16, 32).getBytes(UTF_8)));
}
/**
* Sign a token. This produces a new token, that has this token signed with a nonce.
* <p/>
* This primarily exists to defeat the BREACH vulnerability, as it allows the token to effectively be random per
* request, without actually changing the value.
*
* @param token The token to sign
* @return The signed token
*/
public String signToken(String token) {
long nonce = System.currentTimeMillis();
String joined = nonce + "-" + token;
return sign(joined) + "-" + joined;
}
/**
* Extract a signed token that was signed by {@link #signToken(String)}.
*
* @param token The signed token to extract.
* @return The verified raw token, or null if the token isn't valid.
*/
public String extractSignedToken(String token) {
String[] chunks = token.split("-", 3);
String signature = chunks[0];
String nonce = chunks[1];
String raw = chunks[2];
if (constantTimeEquals(signature, sign(nonce + "-" + raw))) {
return raw;
} else {
return null;
}
}
/**
* Constant time equals method.
* <p/>
* Given a length that both Strings are equal to, this method will always run in constant time.
* This prevents timing attacks.
*/
private boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
} else {
int equal = 0;
for (int i = 0; i < a.length(); i++) {
equal = equal | a.charAt(i) ^ b.charAt(i);
}
return equal == 0;
}
}
/**
* Encode binary data to base64.
*
* @param value The binary data
* @return The base64 encoded String
*/
public String encodeBase64(byte[] value) {
return new String(Base64.encodeBase64(value), UTF_8);
}
/**
* Decode a base64 value.
*
* @param value The base64 encoded String
* @return decoded binary data
*/
public byte[] decodeBase64(String value) {
return Base64.decodeBase64(value.getBytes(UTF_8));
}
/**
* Build an hexadecimal MD5 hash for a String.
*
* @param value The String to hash
* @return An hexadecimal Hash
*/
public String hexMD5(String value) {
try {
MessageDigest messageDigest = MessageDigest.getInstance(Hash.MD5.toString());
messageDigest.reset();
messageDigest.update(value.getBytes(UTF_8));
byte[] digest = messageDigest.digest();
return String.valueOf(Hex.encodeHex(digest));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Build an hexadecimal SHA1 hash for a String.
*
* @param value The String to hash
* @return An hexadecimal Hash
*/
public String hexSHA1(String value) {
try {
MessageDigest md;
md = MessageDigest.getInstance(Hash.SHA1.toString());
md.update(value.getBytes(UTF_8));
byte[] digest = md.digest();
return String.valueOf(Hex.encodeHex(digest));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}