mirror of
https://github.com/danog/termux-api.git
synced 2024-11-30 04:19:20 +01:00
Add KeystoreAPI (#224)
This commit is contained in:
parent
033e4dfdd2
commit
e3a02a5a85
343
app/src/main/java/com/termux/api/KeystoreAPI.java
Normal file
343
app/src/main/java/com/termux/api/KeystoreAPI.java
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
package com.termux.api;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.security.keystore.KeyGenParameterSpec;
|
||||||
|
import android.security.keystore.KeyInfo;
|
||||||
|
import android.security.keystore.KeyProperties;
|
||||||
|
import android.support.annotation.RequiresApi;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.JsonWriter;
|
||||||
|
|
||||||
|
import com.termux.api.util.ResultReturner;
|
||||||
|
import com.termux.api.util.ResultReturner.ResultJsonWriter;
|
||||||
|
import com.termux.api.util.ResultReturner.ResultWriter;
|
||||||
|
import com.termux.api.util.ResultReturner.WithInput;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStore.Entry;
|
||||||
|
import java.security.KeyStore.PrivateKeyEntry;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.security.spec.ECGenParameterSpec;
|
||||||
|
import java.security.spec.RSAKeyGenParameterSpec;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
class KeystoreAPI {
|
||||||
|
// this is the only provider name that is supported by Android
|
||||||
|
private static final String PROVIDER = "AndroidKeyStore";
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
static void onReceive(TermuxApiReceiver apiReceiver, Intent intent) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
// most of the keystore features were added in Android 6
|
||||||
|
printErrorMessage(apiReceiver, intent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (intent.getStringExtra("command")) {
|
||||||
|
case "list":
|
||||||
|
listKeys(apiReceiver, intent);
|
||||||
|
break;
|
||||||
|
case "generate":
|
||||||
|
generateKey(apiReceiver, intent);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
deleteKey(apiReceiver, intent);
|
||||||
|
break;
|
||||||
|
case "sign":
|
||||||
|
signData(apiReceiver, intent);
|
||||||
|
break;
|
||||||
|
case "verify":
|
||||||
|
verifyData(apiReceiver, intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the keys inside the keystore.<br>
|
||||||
|
* Optional intent extras:
|
||||||
|
* <ul>
|
||||||
|
* <li>detailed: if set, key parameters (modulus etc.) are included in the response</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
private static void listKeys(TermuxApiReceiver apiReceiver, final Intent intent) {
|
||||||
|
ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() {
|
||||||
|
@Override
|
||||||
|
public void writeJson(JsonWriter out) throws GeneralSecurityException, IOException {
|
||||||
|
KeyStore keyStore = getKeyStore();
|
||||||
|
Enumeration<String> aliases = keyStore.aliases();
|
||||||
|
boolean detailed = intent.getBooleanExtra("detailed", false);
|
||||||
|
|
||||||
|
out.beginArray();
|
||||||
|
while (aliases.hasMoreElements()) {
|
||||||
|
out.beginObject();
|
||||||
|
|
||||||
|
String alias = aliases.nextElement();
|
||||||
|
out.name("alias").value(alias);
|
||||||
|
|
||||||
|
Entry entry = keyStore.getEntry(alias, null);
|
||||||
|
if (entry instanceof PrivateKeyEntry) {
|
||||||
|
printPrivateKey(out, (PrivateKeyEntry) entry, detailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.endObject();
|
||||||
|
}
|
||||||
|
out.endArray();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for printing the parameters of a given key.
|
||||||
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
private static void printPrivateKey(JsonWriter out, PrivateKeyEntry entry, boolean detailed)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
PrivateKey privateKey = entry.getPrivateKey();
|
||||||
|
String algorithm = privateKey.getAlgorithm();
|
||||||
|
KeyInfo keyInfo = KeyFactory.getInstance(algorithm).getKeySpec(privateKey, KeyInfo.class);
|
||||||
|
|
||||||
|
PublicKey publicKey = entry.getCertificate().getPublicKey();
|
||||||
|
|
||||||
|
out.name("algorithm").value(algorithm);
|
||||||
|
out.name("size").value(keyInfo.getKeySize());
|
||||||
|
|
||||||
|
if (detailed && publicKey instanceof RSAPublicKey) {
|
||||||
|
RSAPublicKey rsa = (RSAPublicKey) publicKey;
|
||||||
|
// convert to hex
|
||||||
|
out.name("modulus").value(rsa.getModulus().toString(16));
|
||||||
|
out.name("exponent").value(rsa.getPublicExponent().toString(16));
|
||||||
|
}
|
||||||
|
if (detailed && publicKey instanceof ECPublicKey) {
|
||||||
|
ECPublicKey ec = (ECPublicKey) publicKey;
|
||||||
|
// convert to hex
|
||||||
|
out.name("x").value(ec.getW().getAffineX().toString(16));
|
||||||
|
out.name("y").value(ec.getW().getAffineY().toString(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.name("inside_secure_hardware").value(keyInfo.isInsideSecureHardware());
|
||||||
|
|
||||||
|
out.name("user_authentication");
|
||||||
|
|
||||||
|
out.beginObject();
|
||||||
|
out.name("required").value(keyInfo.isUserAuthenticationRequired());
|
||||||
|
|
||||||
|
out.name("enforced_by_secure_hardware");
|
||||||
|
out.value(keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware());
|
||||||
|
|
||||||
|
int validityDuration = keyInfo.getUserAuthenticationValidityDurationSeconds();
|
||||||
|
if (validityDuration >= 0) {
|
||||||
|
out.name("validity_duration_seconds").value(validityDuration);
|
||||||
|
}
|
||||||
|
out.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently delete a key from the keystore.<br>
|
||||||
|
* Required intent extras:
|
||||||
|
* <ul>
|
||||||
|
* <li>alias: key alias</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private static void deleteKey(TermuxApiReceiver apiReceiver, final Intent intent) {
|
||||||
|
ResultReturner.returnData(apiReceiver, intent, new ResultWriter() {
|
||||||
|
@Override
|
||||||
|
public void writeResult(PrintWriter out) throws IOException, GeneralSecurityException {
|
||||||
|
String alias = intent.getStringExtra("alias");
|
||||||
|
// unfortunately this statement does not return anything
|
||||||
|
// nor does it throw an exception if the alias does not exist
|
||||||
|
getKeyStore().deleteEntry(alias);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new key inside the keystore.<br>
|
||||||
|
* Required intent extras:
|
||||||
|
* <ul>
|
||||||
|
* <li>alias: key alias</li>
|
||||||
|
* <li>
|
||||||
|
* algorithm: key algorithm, should be one of the KeyProperties.KEY_ALGORITHM_*
|
||||||
|
* values, for example {@link KeyProperties#KEY_ALGORITHM_RSA} or
|
||||||
|
* {@link KeyProperties#KEY_ALGORITHM_EC}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* purposes: purposes of this key, should be a combination of
|
||||||
|
* KeyProperties.PURPOSE_*, for example 12 for
|
||||||
|
* {@link KeyProperties#PURPOSE_SIGN}+{@link KeyProperties#PURPOSE_VERIFY}
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* digests: set of hashes this key can be used with, should be an array of
|
||||||
|
* KeyProperties.DIGEST_* values, for example
|
||||||
|
* {@link KeyProperties#DIGEST_SHA256} and {@link KeyProperties#DIGEST_SHA512}
|
||||||
|
* </li>
|
||||||
|
* <li>size: key size, only used for RSA keys</li>
|
||||||
|
* <li>curve: elliptic curve name, only used for EC keys</li>
|
||||||
|
* <li>
|
||||||
|
* userValidity: number of seconds where it is allowed to use this key for signing
|
||||||
|
* after unlocking the device (re-locking and unlocking restarts the timer), if set to 0
|
||||||
|
* this feature is disabled (i.e. the key can be used anytime)
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
private static void generateKey(TermuxApiReceiver apiReceiver, final Intent intent) {
|
||||||
|
ResultReturner.returnData(apiReceiver, intent, new ResultWriter() {
|
||||||
|
@Override
|
||||||
|
public void writeResult(PrintWriter out) throws GeneralSecurityException {
|
||||||
|
String alias = intent.getStringExtra("alias");
|
||||||
|
String algorithm = intent.getStringExtra("algorithm");
|
||||||
|
int purposes = intent.getIntExtra("purposes", 0);
|
||||||
|
String[] digests = intent.getStringArrayExtra("digests");
|
||||||
|
int size = intent.getIntExtra("size", 2048);
|
||||||
|
String curve = intent.getStringExtra("curve");
|
||||||
|
int userValidity = intent.getIntExtra("validity", 0);
|
||||||
|
|
||||||
|
KeyGenParameterSpec.Builder builder =
|
||||||
|
new KeyGenParameterSpec.Builder(alias, purposes);
|
||||||
|
|
||||||
|
builder.setDigests(digests);
|
||||||
|
if (algorithm.equals(KeyProperties.KEY_ALGORITHM_RSA)) {
|
||||||
|
// only the exponent 65537 is supported for now
|
||||||
|
builder.setAlgorithmParameterSpec(
|
||||||
|
new RSAKeyGenParameterSpec(size, RSAKeyGenParameterSpec.F4));
|
||||||
|
builder.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (algorithm.equals(KeyProperties.KEY_ALGORITHM_EC)) {
|
||||||
|
builder.setAlgorithmParameterSpec(new ECGenParameterSpec(curve));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userValidity > 0) {
|
||||||
|
builder.setUserAuthenticationRequired(true);
|
||||||
|
builder.setUserAuthenticationValidityDurationSeconds(userValidity);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm, PROVIDER);
|
||||||
|
generator.initialize(builder.build());
|
||||||
|
generator.generateKeyPair();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a given byte stream. The file is read from stdin and the signature is output to stdout.
|
||||||
|
* The output is encoded using base64.<br>
|
||||||
|
* Required intent extras:
|
||||||
|
* <ul>
|
||||||
|
* <li>alias: key alias</li>
|
||||||
|
* <li>
|
||||||
|
* algorithm: key algorithm and hash combination to use, e.g. SHA512withRSA
|
||||||
|
* (the full list can be found at
|
||||||
|
* <a href="https://developer.android.com/training/articles/keystore#SupportedSignatures">
|
||||||
|
* the Android documentation</a>)
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private static void signData(TermuxApiReceiver apiReceiver, final Intent intent) {
|
||||||
|
ResultReturner.returnData(apiReceiver, intent, new WithInput() {
|
||||||
|
@Override
|
||||||
|
public void writeResult(PrintWriter out) throws Exception {
|
||||||
|
String alias = intent.getStringExtra("alias");
|
||||||
|
String algorithm = intent.getStringExtra("algorithm");
|
||||||
|
byte[] input = readStream(in);
|
||||||
|
|
||||||
|
PrivateKeyEntry key = (PrivateKeyEntry) getKeyStore().getEntry(alias, null);
|
||||||
|
Signature signature = Signature.getInstance(algorithm);
|
||||||
|
signature.initSign(key.getPrivateKey());
|
||||||
|
signature.update(input);
|
||||||
|
byte[] outputData = signature.sign();
|
||||||
|
|
||||||
|
// we are not allowed to output bytes in this function
|
||||||
|
// one option is to encode using base64 which is a plain string
|
||||||
|
out.write(Base64.encodeToString(outputData, Base64.NO_WRAP));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a given byte stream along with a signature file.
|
||||||
|
* The file is read from stdin, and a "true" or "false" message is printed to the stdout.<br>
|
||||||
|
* Required intent extras:
|
||||||
|
* <ul>
|
||||||
|
* <li>alias: key alias</li>
|
||||||
|
* <li>
|
||||||
|
* algorithm: key algorithm and hash combination that was used to create this signature,
|
||||||
|
* e.g. SHA512withRSA (the full list can be found at
|
||||||
|
* <a href="https://developer.android.com/training/articles/keystore#SupportedSignatures">
|
||||||
|
* the Android documentation</a>)
|
||||||
|
* </li>
|
||||||
|
* <li>signature: path of the signature file</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private static void verifyData(TermuxApiReceiver apiReceiver, final Intent intent) {
|
||||||
|
ResultReturner.returnData(apiReceiver, intent, new WithInput() {
|
||||||
|
@Override
|
||||||
|
public void writeResult(PrintWriter out) throws GeneralSecurityException, IOException {
|
||||||
|
String alias = intent.getStringExtra("alias");
|
||||||
|
String algorithm = intent.getStringExtra("algorithm");
|
||||||
|
byte[] input = readStream(in);
|
||||||
|
File signatureFile = new File(intent.getStringExtra("signature"));
|
||||||
|
|
||||||
|
byte[] signatureData = new byte[(int) signatureFile.length()];
|
||||||
|
int read = new FileInputStream(signatureFile).read(signatureData);
|
||||||
|
if (signatureFile.length() != read) out.println(false);
|
||||||
|
|
||||||
|
Signature signature = Signature.getInstance(algorithm);
|
||||||
|
signature.initVerify(getKeyStore().getCertificate(alias).getPublicKey());
|
||||||
|
signature.update(input);
|
||||||
|
boolean verified = signature.verify(signatureData);
|
||||||
|
|
||||||
|
out.println(verified);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up and return the keystore.
|
||||||
|
*/
|
||||||
|
private static KeyStore getKeyStore() throws GeneralSecurityException, IOException {
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(PROVIDER);
|
||||||
|
keyStore.load(null);
|
||||||
|
return keyStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a given stream to a byte array. Should not be used with large streams.
|
||||||
|
*/
|
||||||
|
private static byte[] readStream(InputStream stream) throws IOException {
|
||||||
|
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int read;
|
||||||
|
while ((read = stream.read(buffer)) > 0) {
|
||||||
|
byteStream.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
return byteStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printErrorMessage(TermuxApiReceiver apiReceiver, Intent intent) {
|
||||||
|
ResultReturner.returnData(apiReceiver, intent, new ResultWriter() {
|
||||||
|
@Override
|
||||||
|
public void writeResult(PrintWriter out) {
|
||||||
|
out.println("termux-keystore requires at least Android 6.0 (Marshmallow).");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -83,6 +83,9 @@ public class TermuxApiReceiver extends BroadcastReceiver {
|
|||||||
InfraredAPI.onReceiveTransmit(this, context, intent);
|
InfraredAPI.onReceiveTransmit(this, context, intent);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "Keystore":
|
||||||
|
KeystoreAPI.onReceive(this, intent);
|
||||||
|
break;
|
||||||
case "Location":
|
case "Location":
|
||||||
if (TermuxApiPermissionActivity.checkAndRequestPermissions(context, intent, Manifest.permission.ACCESS_FINE_LOCATION)) {
|
if (TermuxApiPermissionActivity.checkAndRequestPermissions(context, intent, Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||||
LocationAPI.onReceive(this, context, intent);
|
LocationAPI.onReceive(this, context, intent);
|
||||||
|
Loading…
Reference in New Issue
Block a user