package co.gem.round.crypto;
import co.gem.round.encoding.Hex;
import org.spongycastle.crypto.*;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.spongycastle.crypto.modes.CBCBlockCipher;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.paddings.ZeroBytePadding;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import org.spongycastle.util.Arrays;
import org.abstractj.kalium.crypto.SecretBox;
import javax.crypto.*;
import javax.crypto.Mac;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
public class PassphraseBox {
public enum Mode { AES, SODIUM }
private byte[] aesKey;
private byte[] salt;
private byte[] iv;
private BufferedBlockCipher encryptCipher;
private BufferedBlockCipher decryptCipher;
private SecretKeySpec aesSecretKey;
private SecretKeySpec hmacSecretKey;
private int iterations;
private SecureRandom random;
private SecretBox box;
private Mode mode;
final int IVBYTES = 16;
final int SALTBYTES = 16;
final int KEYBYTES = 32;
public static final String UTF_8 = "UTF-8";
final int ITERATIONS = 100000;
final int ITERATIONS_WINDOW = 20000;
static {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
public PassphraseBox(String passphrase, String salt, int iterations, Mode mode) throws
NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException,
InvalidKeyException, NoSuchProviderException {
this.mode = mode;
random = new SecureRandom();
if (salt == null) {
this.salt = new byte[SALTBYTES];
random.nextBytes(this.salt);
} else {
this.salt = Hex.decode(salt);
}
if (iterations == 0) {
int randomWindow = random.nextInt(ITERATIONS_WINDOW);
this.iterations = ITERATIONS + randomWindow;
} else {
this.iterations = iterations;
}
if (this.mode == Mode.AES) {
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator();
generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(passphrase.toCharArray()), this.salt, this.iterations);
byte[] key = ((KeyParameter) generator.generateDerivedParameters(KEYBYTES * 2 * 8)).getKey();
this.aesKey = Arrays.copyOfRange(key, 0, KEYBYTES);
this.aesSecretKey = new SecretKeySpec(aesKey, "AES");
this.hmacSecretKey = new SecretKeySpec(Arrays.copyOfRange(key, KEYBYTES, KEYBYTES * 2), "HmacSHA256");
this.encryptCipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new ZeroBytePadding());
this.decryptCipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new ZeroBytePadding());
} else if (this.mode == Mode.SODIUM) {
PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), this.salt, iterations, 32 * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] key = skf.generateSecret(spec).getEncoded();
this.box = new SecretBox(key);
}
}
private byte[] cipherData(BufferedBlockCipher cipher, byte[] data) throws
InvalidCipherTextException, UnsupportedEncodingException {
byte[] outBuf = new byte[cipher.getOutputSize(data.length)];
int length = cipher.processBytes(data, 0, data.length, outBuf, 0);
length += cipher.doFinal(outBuf, length);
byte[] out = new byte[length];
System.arraycopy(outBuf, 0, out, 0, length);
return out;
}
public String decrypt(String iv, String nonce, String ciphertext) throws
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException, UnsupportedEncodingException, InvalidCipherTextException,
NoSuchAlgorithmException {
if (this.mode == Mode.AES) {
return decryptAes(iv, ciphertext);
} else if (this.mode == Mode.SODIUM) {
return decryptSodium(nonce, ciphertext);
} else {
throw new NoSuchAlgorithmException();
}
}
public String decryptAes(String iv, String ciphertext) throws
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException, UnsupportedEncodingException, InvalidCipherTextException,
NoSuchAlgorithmException {
this.iv = Hex.decode(iv);
byte[] ctext = Hex.decode(ciphertext);
// This "ciphertext" that we import is constructed from actual_ciphertext + hmacsha1(iv + actual_ciphertext)
byte[] ctextb = Arrays.copyOfRange(ctext, 0, ctext.length - 32);
byte[] mac = Arrays.copyOfRange(ctext, ctext.length - 32, ctext.length);
// Recreate the hmac and verify it matches.
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(this.hmacSecretKey);
if (!Arrays.areEqual(mac, hmac.doFinal(Arrays.concatenate(this.iv, ctextb)))) {
throw new RuntimeException("Invalid authentication code: ciphertext may have been tampered with.");
}
// Decrypt the actual_ciphertext.
CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(aesKey), this.iv);
decryptCipher.init(false, ivAndKey);
return new String(cipherData(decryptCipher, ctextb), UTF_8);
}
public String decryptSodium(String nonce, String ciphertext) {
byte[] nonceBytes = Hex.decode(nonce);
byte[] ciphertextBytes = Hex.decode(ciphertext);
String message = new String(this.box.decrypt(nonceBytes, ciphertextBytes));
return message;
}
public EncryptedMessage encrypt(String message) throws
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException,
InvalidCipherTextException, NoSuchAlgorithmException {
this.iv = new byte[IVBYTES];
random.nextBytes(this.iv);
CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(aesKey), this.iv);
encryptCipher.init(true, ivAndKey);
byte[] es = cipherData(encryptCipher, message.getBytes(UTF_8));
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(this.hmacSecretKey);
byte[] digest = hmac.doFinal(Arrays.concatenate(this.iv, es));
byte[] ciphertext = Arrays.concatenate(es, digest);
EncryptedMessage encrypted = new EncryptedMessage();
encrypted.ciphertext = Hex.encode(ciphertext);
encrypted.iv = Hex.encode(this.iv);
encrypted.salt = Hex.encode(this.salt);
encrypted.iterations = iterations;
return encrypted;
}
public static String decrypt(String passphrase, EncryptedMessage encryptedMessage) throws
NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException,
NoSuchPaddingException, InvalidAlgorithmParameterException, BadPaddingException,
IllegalBlockSizeException, NoSuchProviderException, UnsupportedEncodingException, InvalidCipherTextException {
Mode mode = null;
if (encryptedMessage.iv != null)
mode = Mode.AES;
else if (encryptedMessage.nonce != null)
mode = Mode.SODIUM;
else
throw new NoSuchAlgorithmException();
PassphraseBox box = new PassphraseBox(passphrase, encryptedMessage.salt, encryptedMessage.iterations, mode);
return box.decrypt(encryptedMessage.iv, encryptedMessage.nonce, encryptedMessage.ciphertext);
}
public static EncryptedMessage encrypt(String passphrase, String message) throws
NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException,
NoSuchPaddingException, InvalidAlgorithmParameterException, BadPaddingException,
IllegalBlockSizeException, NoSuchProviderException, UnsupportedEncodingException, InvalidCipherTextException {
PassphraseBox box = new PassphraseBox(passphrase, null, 0, Mode.AES);
return box.encrypt(message);
}
}