/*
* Copyright (C) 2015 Steven Luo
*
* 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.
*/
package jackpal.androidterm.util;
import jackpal.androidterm.compat.Base64;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Implementation of a simple authenticated encryption scheme suitable for
* TEA shortcuts.
*
* The goals of the encryption are as follows:
*
* (1) An unauthorized actor must not be able to create a valid text with
* contents of his choice;
* (2) An unauthorized actor must not be able to modify an existing text to
* change its contents in any way;
* (3) An unauthorized actor must not be able to discover the contents of
* an existing text.
*
* Conditions (1) and (2) ensure that an attacker cannot send commands of his
* choosing to TEA via the shortcut mechanism, while condition (3) ensures that
* an attacker cannot learn what commands are being sent via shortcuts even if
* he can read saved shortcuts or sniff Android intents.
*
* We ensure these conditions using two cryptographic building blocks:
*
* * a symmetric cipher (currently AES in CBC mode using PKCS#5 padding),
* which prevents someone without the encryption key from reading the
* contents of the shortcut; and
* * a message authentication code (currently HMAC-SHA256), which proves that
* the shortcut was created by someone with the MAC key.
*
* The security of these depends on the security of the keys, which must be
* kept secret. In this application, the keys are randomly generated and stored
* in the application's private shared preferences.
*
* The encrypted string output by this scheme is of the form:
*
* mac + ":" + iv + ":" cipherText
*
* where:
*
* * cipherText is the Base64-encoded result of encrypting the data
* using the encryption key;
* * iv is a Base64-encoded, non-secret random number used as an
* initialization vector for the encryption algorithm;
* * mac is the Base64 encoding of MAC(MAC-key, iv + ":" + cipherText).
*/
public final class ShortcutEncryption {
public static final String ENC_ALGORITHM = "AES";
public static final String ENC_SYSTEM = ENC_ALGORITHM + "/CBC/PKCS5Padding";
public static final int ENC_BLOCKSIZE = 16;
public static final String MAC_ALGORITHM = "HmacSHA256";
public static final int KEYLEN = 128;
public static final int BASE64_DFLAGS = Base64.DEFAULT;
public static final int BASE64_EFLAGS = Base64.NO_PADDING | Base64.NO_WRAP;
private static final String SHORTCUT_KEYS_PREF = "shortcut_keys";
private static final Pattern COLON = Pattern.compile(":");
public static final class Keys {
private final SecretKey encKey;
private final SecretKey macKey;
public Keys(SecretKey encKey, SecretKey macKey) {
this.encKey = encKey;
this.macKey = macKey;
}
public SecretKey getEncKey() {
return encKey;
}
public SecretKey getMacKey() {
return macKey;
}
/**
* Outputs the keys as a string of the form
*
* encKey + ":" + macKey
*
* where encKey and macKey are the Base64-encoded encryption and MAC
* keys.
*/
public String encode() {
return encodeToBase64(encKey.getEncoded()) + ":" + encodeToBase64(macKey.getEncoded());
}
/**
* Creates a new Keys object by decoding a string of the form output
* from encode().
*/
public static Keys decode(String encodedKeys) {
String[] keys = COLON.split(encodedKeys);
if (keys.length != 2) {
throw new IllegalArgumentException("Invalid encoded keys!");
}
SecretKey encKey = new SecretKeySpec(decodeBase64(keys[0]), ENC_ALGORITHM);
SecretKey macKey = new SecretKeySpec(decodeBase64(keys[1]), MAC_ALGORITHM);
return new Keys(encKey, macKey);
}
}
/**
* Retrieves the shortcut encryption keys from preferences.
*/
public static Keys getKeys(Context ctx) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
String keyEnc = prefs.getString(SHORTCUT_KEYS_PREF, null);
if (keyEnc == null) {
return null;
}
try {
return Keys.decode(keyEnc);
} catch (IllegalArgumentException e) {
return null;
}
}
/**
* Saves shortcut encryption keys to preferences.
*/
public static void saveKeys(Context ctx, Keys keys) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
SharedPreferences.Editor edit = prefs.edit();
edit.putString(SHORTCUT_KEYS_PREF, keys.encode());
edit.commit();
}
/**
* Generates new secret keys suitable for the encryption scheme described
* above.
*
* @throws GeneralSecurityException if an error occurs during key generation.
*/
public static Keys generateKeys() throws GeneralSecurityException {
KeyGenerator gen = KeyGenerator.getInstance(ENC_ALGORITHM);
gen.init(KEYLEN);
SecretKey encKey = gen.generateKey();
/* XXX: It's probably unnecessary to create a different keygen for the
* MAC, but JCA's API design suggests we should just in case ... */
gen = KeyGenerator.getInstance(MAC_ALGORITHM);
gen.init(KEYLEN);
SecretKey macKey = gen.generateKey();
return new Keys(encKey, macKey);
}
/**
* Decrypts a string encrypted using this algorithm and verifies that the
* contents have not been tampered with.
*
* @param encrypted The string to decrypt, in the format described above.
* @param keys The keys to verify and decrypt with.
* @return The decrypted data.
*
* @throws GeneralSecurityException if the data is invalid, verification fails, or an error occurs during decryption.
*/
public static String decrypt(String encrypted, Keys keys) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(ENC_SYSTEM);
String[] data = COLON.split(encrypted);
if (data.length != 3) {
throw new GeneralSecurityException("Invalid encrypted data!");
}
String mac = data[0];
String iv = data[1];
String cipherText = data[2];
// Verify that the ciphertext and IV haven't been tampered with first
String dataToAuth = iv + ":" + cipherText;
if (!computeMac(dataToAuth, keys.getMacKey()).equals(mac)) {
throw new GeneralSecurityException("Incorrect MAC!");
}
// Decrypt the ciphertext
byte[] ivBytes = decodeBase64(iv);
cipher.init(Cipher.DECRYPT_MODE, keys.getEncKey(), new IvParameterSpec(ivBytes));
byte[] bytes = cipher.doFinal(decodeBase64(cipherText));
// Decode the plaintext bytes into a String
CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPORT);
decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
/*
* We are coding UTF-8 (guaranteed to be the default charset on
* Android) to Java chars (UTF-16, 2 bytes per char). For valid UTF-8
* sequences, then:
* 1 byte in UTF-8 (US-ASCII) -> 1 char in UTF-16
* 2-3 bytes in UTF-8 (BMP) -> 1 char in UTF-16
* 4 bytes in UTF-8 (non-BMP) -> 2 chars in UTF-16 (surrogate pair)
* The decoded output is therefore guaranteed to fit into a char
* array the same length as the input byte array.
*/
CharBuffer out = CharBuffer.allocate(bytes.length);
CoderResult result = decoder.decode(ByteBuffer.wrap(bytes), out, true);
if (result.isError()) {
/* The input was supposed to be the result of encrypting a String,
* so something is very wrong if it cannot be decoded into one! */
throw new GeneralSecurityException("Corrupt decrypted data!");
}
decoder.flush(out);
return out.flip().toString();
}
/**
* Encrypts and authenticates a string using the algorithm described above.
*
* @param data The string containing the data to encrypt.
* @param keys The keys to encrypt and authenticate with.
* @return The encrypted data.
*
* @throws GeneralSecurityException if an error occurs during encryption.
*/
public static String encrypt(String data, Keys keys) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(ENC_SYSTEM);
// Generate a random IV
SecureRandom rng = new SecureRandom();
byte[] ivBytes = new byte[ENC_BLOCKSIZE];
rng.nextBytes(ivBytes);
String iv = encodeToBase64(ivBytes);
// Encrypt
cipher.init(Cipher.ENCRYPT_MODE, keys.getEncKey(), new IvParameterSpec(ivBytes));
byte[] bytes = data.getBytes();
String cipherText = encodeToBase64(cipher.doFinal(bytes));
// Calculate the MAC for the ciphertext and IV
String dataToAuth = iv + ":" + cipherText;
String mac = computeMac(dataToAuth, keys.getMacKey());
return mac + ":" + dataToAuth;
}
/**
* Computes the Base64-encoded Message Authentication Code for the
* data using the provided key.
*
* @throws GeneralSecurityException if an error occurs during MAC computation.
*/
private static String computeMac(String data, SecretKey key) throws GeneralSecurityException {
Mac mac = Mac.getInstance(MAC_ALGORITHM);
mac.init(key);
byte[] macBytes = mac.doFinal(data.getBytes());
return encodeToBase64(macBytes);
}
/**
* Encodes binary data to Base64 using the settings specified by
* BASE64_EFLAGS.
*
* @return A String with the Base64-encoded data.
*/
private static String encodeToBase64(byte[] data) {
return Base64.encodeToString(data, BASE64_EFLAGS);
}
/**
* Decodes Base64-encoded binary data using the settings specified by
* BASE64_DFLAGS.
*
* @param data A String with the Base64-encoded data.
* @return A newly-allocated byte[] array with the decoded data.
*/
private static byte[] decodeBase64(String data) {
return Base64.decode(data, BASE64_DFLAGS);
}
// Prevent instantiation
private ShortcutEncryption() {
throw new UnsupportedOperationException();
}
}