package org.emergent.android.weave.client;
import java.io.ByteArrayInputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @author Patrick Woodworth
*/
class WeaveCryptoUtil {
private static final String PROVIDER_NAME = "BC";
private static final String PROVIDER_CLASS = "org.bouncycastle.jce.provider.BouncyCastleProvider";
private static final byte[] HMAC_INPUT = WeaveUtil
.toAsciiBytes("Sync-AES_256_CBC-HMAC256");
private static final Pattern ILLEGAL_USERNAME_PATTERN = Pattern.compile(
"[^A-Z0-9._-]", Pattern.CASE_INSENSITIVE);
private static final WeaveCryptoUtil sm_instance = new WeaveCryptoUtil();
static {
initProvider(PROVIDER_NAME, PROVIDER_CLASS);
}
/**
* This code basically inlines usage of the BouncyCastle private API and is
* equivalent to the following code.
*
* <pre>
* {
* @code
* PBEParametersGenerator generator = new PKCS5S2ParametersGenerator();
* generator.init(PBEParametersGenerator.PKCS5PasswordToBytes(secret), salt,
* 4096);
* CipherParameters keyParam = generator.generateDerivedParameters(256);
* return ((KeyParameter) keyParam).getKey();
* }
* </pre>
*/
private static byte[] derivePKCS5S2(char[] secret, byte[] salt)
throws GeneralSecurityException {
byte[] secretBytes = passwordPKCS5ToBytes(secret);
int keySizeInBytes = 256 / 8;
final int iterations = 4096;
Mac hMac = Mac.getInstance("HMACSHA1");
int hLen = hMac.getMacLength();
int l = (keySizeInBytes + hLen - 1) / hLen;
byte[] iBuf = new byte[4];
byte[] dKey = new byte[l * hLen];
for (int i = 1; i <= l; i++) {
intToOctet(iBuf, i);
derivePKCS5S2Helper(hMac, secretBytes, salt, iterations, iBuf,
dKey, (i - 1) * hLen);
}
byte[] retval = new byte[keySizeInBytes];
System.arraycopy(dKey, 0, retval, 0, keySizeInBytes);
return retval;
}
private static void derivePKCS5S2Helper(Mac hMac, byte[] P, byte[] S,
int c, byte[] iBuf, byte[] out, int outOff)
throws GeneralSecurityException {
byte[] state = new byte[hMac.getMacLength()];
SecretKeySpec param = new SecretKeySpec(P, "SHA1");
hMac.init(param);
if (S != null) {
hMac.update(S, 0, S.length);
}
hMac.update(iBuf, 0, iBuf.length);
hMac.doFinal(state, 0);
System.arraycopy(state, 0, out, outOff, state.length);
if (c == 0) {
throw new IllegalArgumentException(
"iteration count must be at least 1.");
}
for (int count = 1; count < c; count++) {
hMac.init(param);
hMac.update(state, 0, state.length);
hMac.doFinal(state, 0);
for (int j = 0; j != state.length; j++) {
out[outOff + j] ^= state[j];
}
}
}
public static byte[] deriveSyncHmacKey(byte[] secretBytes, byte[] bkbytes,
String username) throws GeneralSecurityException {
int keySizeInBytes = 256 / 8;
Mac hMac = Mac.getInstance("HMACSHA256");
byte[] state = new byte[hMac.getMacLength()];
SecretKeySpec param = new SecretKeySpec(secretBytes, "SHA256");
hMac.init(param);
hMac.update(bkbytes);
// hMac.update(WeaveUtil.toAsciiBytes(bkstr));
hMac.update(HMAC_INPUT);
hMac.update(WeaveUtil.toAsciiBytes(username));
hMac.update((byte) 0x2);
hMac.doFinal(state, 0);
byte[] retval = new byte[keySizeInBytes];
System.arraycopy(state, 0, retval, 0, keySizeInBytes);
return retval;
}
public static byte[] deriveSyncKey(byte[] secretBytes, String username)
throws GeneralSecurityException {
int keySizeInBytes = 256 / 8;
Mac hMac = Mac.getInstance("HMACSHA256");
byte[] state = new byte[hMac.getMacLength()];
SecretKeySpec param = new SecretKeySpec(secretBytes, "SHA256");
hMac.init(param);
hMac.update(HMAC_INPUT);
hMac.update(WeaveUtil.toAsciiBytes(username));
hMac.update((byte) 0x1);
hMac.doFinal(state, 0);
byte[] retval = new byte[keySizeInBytes];
System.arraycopy(state, 0, retval, 0, keySizeInBytes);
return retval;
}
public static WeaveCryptoUtil getInstance() {
return sm_instance;
}
@SuppressWarnings("unchecked")
protected static boolean initProvider(String providerName, String className) {
try {
Provider provider = Security.getProvider(providerName);
if (provider == null) {
Class clazz = Class.forName(className);
provider = (Provider) clazz.newInstance();
Security.addProvider(provider);
}
return true;
} catch (Throwable ignored) {
}
return false;
}
private static void intToOctet(byte[] buf, int i) {
buf[0] = (byte) (i >>> 24);
buf[1] = (byte) (i >>> 16);
buf[2] = (byte) (i >>> 8);
buf[3] = (byte) i;
}
private static byte[] passwordPKCS5ToBytes(char[] password) {
byte[] bytes = new byte[password.length];
for (int ii = 0; ii != bytes.length; ii++) {
bytes[ii] = (byte) password[ii];
}
return bytes;
}
private WeaveCryptoUtil() {
}
private void checkMac(Key secKey, String ciphertext, String hmac)
throws GeneralSecurityException {
String hmac2 = createMac(secKey, ciphertext);
if (!hmac.equalsIgnoreCase(hmac2))
throw new GeneralSecurityException("mac failed");
}
private String createMac(Key secKey, String ciphertext)
throws GeneralSecurityException {
Mac mac = Mac.getInstance("HMACSHA256", PROVIDER_NAME);
// mac.init(new SecretKeySpec(Base64.encode(secKey.getEncoded()),
// "AES"));
mac.init(secKey);
byte[] hmacBytes = mac.doFinal(WeaveUtil.toAsciiBytes(ciphertext));
return WeaveUtil.toAsciiString(Hex.encode(hmacBytes));
}
public RSAPrivateKey decodePrivateKeyFromPKCSBytes(byte[] keySpecBytes)
throws GeneralSecurityException {
KeySpec keySpec = new PKCS8EncodedKeySpec(keySpecBytes);
KeyFactory keyFact = KeyFactory.getInstance("RSA", PROVIDER_NAME);
return (RSAPrivateKey) keyFact.generatePrivate(keySpec);
}
public byte[] decrypt(Key secKey, Key hmacKey, String ciphertext,
String iv, String hmac) throws GeneralSecurityException {
checkMac(hmacKey, ciphertext, hmac);
byte[] ciphertextbytes = Base64.decode(ciphertext);
byte[] ivBytes = Base64.decode(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING",
PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secKey, new IvParameterSpec(ivBytes));
return cipher.doFinal(ciphertextbytes);
}
public byte[] decrypt(Key secKey, String ciphertext, String iv, String hmac)
throws GeneralSecurityException {
// checkMac(secKey, ciphertext, hmac);
byte[] ciphertextbytes = Base64.decode(ciphertext);
byte[] ivBytes = Base64.decode(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING",
PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secKey, new IvParameterSpec(ivBytes));
return cipher.doFinal(ciphertextbytes);
}
private byte[] encrypt(Key secKey, byte[] plaintext, String iv)
throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING",
PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secKey,
new IvParameterSpec(Base64.decode(iv)));
return cipher.doFinal(plaintext);
}
@SuppressWarnings("unused")
private byte[] encrypt(Key secKey, String plaintext, String iv)
throws GeneralSecurityException {
byte[] plaintextbytes = Base64.decode(plaintext);
return encrypt(secKey, plaintextbytes, iv);
}
protected Key getKeyDecryptionKey(char[] secret, byte[] salt)
throws GeneralSecurityException {
byte[] keyBytes = derivePKCS5S2(secret, salt);
return new SecretKeySpec(keyBytes, "AES");
}
public String legalizeUsername(String friendlyUsername) {
try {
if (!ILLEGAL_USERNAME_PATTERN.matcher(friendlyUsername).find())
return friendlyUsername;
MessageDigest digest = MessageDigest.getInstance("SHA1");
digest.update(WeaveUtil.toAsciiBytes(friendlyUsername.toLowerCase()));
byte[] baseEncodedBytes = Base32.encode(digest.digest());
return WeaveUtil.toAsciiString(baseEncodedBytes);
} catch (GeneralSecurityException e) {
throw new Error(e);
}
}
public X509Certificate readCertificate(byte[] certBytes)
throws GeneralSecurityException {
CertificateFactory certFact = CertificateFactory.getInstance("X.509",
PROVIDER_NAME);
return (X509Certificate) certFact
.generateCertificate(new ByteArrayInputStream(certBytes));
}
public RSAPublicKey readCertificatePubKey(String base64EncodedCert)
throws GeneralSecurityException {
byte[] certBytes = Base64.decode(base64EncodedCert);
// X509EncodedKeySpec keySpec = new X509EncodedKeySpec(certBytes);
// KeyFactory certFact = KeyFactory.getInstance("RSA", PROVIDER_NAME);
// return (RSAPublicKey)certFact.generatePublic(keySpec);
return (RSAPublicKey) readCertificate(certBytes);
}
public byte[] readPrivateKeyToPKCSBytes(char[] encpass, String salt,
String iv, String keyData) throws GeneralSecurityException {
Key key = getKeyDecryptionKey(encpass, Base64.decode(salt));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING",
PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, key,
new IvParameterSpec(Base64.decode(iv)));
return cipher.doFinal(Base64.decode(keyData));
}
public Key unwrapSecretKey(RSAPrivateKey privKey, String wrapped)
throws GeneralSecurityException {
byte[] wrappedBytes = Base64.decode(wrapped);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding",
PROVIDER_NAME);
cipher.init(Cipher.UNWRAP_MODE, privKey);
return cipher.unwrap(wrappedBytes, "AES", Cipher.SECRET_KEY);
}
}