mirror of
https://github.com/moparisthebest/open-keychain
synced 2024-12-26 00:48:51 -05:00
Merge branch 'certify' into certs
Conflicts: OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java OpenPGP-Keychain/src/main/res/values/strings.xml
This commit is contained in:
commit
6bacb5ff51
@ -17,7 +17,12 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.helper;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
@ -60,6 +65,63 @@ public class OtherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static SpannableStringBuilder colorizeFingerprint(String fingerprint) {
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder(fingerprint);
|
||||
try {
|
||||
// for each 4 characters of the fingerprint + 1 space
|
||||
for (int i = 0; i < fingerprint.length(); i += 5) {
|
||||
int spanEnd = Math.min(i + 4, fingerprint.length());
|
||||
String fourChars = fingerprint.substring(i, spanEnd);
|
||||
|
||||
int raw = Integer.parseInt(fourChars, 16);
|
||||
byte[] bytes = {(byte) ((raw >> 8) & 0xff - 128), (byte) (raw & 0xff - 128)};
|
||||
int[] color = OtherHelper.getRgbForData(bytes);
|
||||
int r = color[0];
|
||||
int g = color[1];
|
||||
int b = color[2];
|
||||
|
||||
// we cannot change black by multiplication, so adjust it to an almost-black grey,
|
||||
// which will then be brightened to the minimal brightness level
|
||||
if (r == 0 && g == 0 && b == 0) {
|
||||
r = 1;
|
||||
g = 1;
|
||||
b = 1;
|
||||
}
|
||||
|
||||
// Convert rgb to brightness
|
||||
double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
|
||||
// If a color is too dark to be seen on black,
|
||||
// then brighten it up to a minimal brightness.
|
||||
if (brightness < 80) {
|
||||
double factor = 80.0 / brightness;
|
||||
r = Math.min(255, (int) (r * factor));
|
||||
g = Math.min(255, (int) (g * factor));
|
||||
b = Math.min(255, (int) (b * factor));
|
||||
|
||||
// If it is too light, then darken it to a respective maximal brightness.
|
||||
} else if (brightness > 180) {
|
||||
double factor = 180.0 / brightness;
|
||||
r = (int) (r * factor);
|
||||
g = (int) (g * factor);
|
||||
b = (int) (b * factor);
|
||||
}
|
||||
|
||||
// Create a foreground color with the 3 digest integers as RGB
|
||||
// and then converting that int to hex to use as a color
|
||||
sb.setSpan(new ForegroundColorSpan(Color.rgb(r, g, b)),
|
||||
i, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "Colorization failed", e);
|
||||
// if anything goes wrong, then just display the fingerprint without colour,
|
||||
// instead of partially correct colour or wrong colours
|
||||
return new SpannableStringBuilder(fingerprint);
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given bytes to a unique RGB color using SHA1 algorithm
|
||||
*
|
||||
|
@ -36,6 +36,7 @@ import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Primes;
|
||||
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
|
||||
@ -46,6 +47,8 @@ import java.security.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class PgpKeyOperation {
|
||||
@ -198,7 +201,7 @@ public class PgpKeyOperation {
|
||||
|
||||
public void buildSecretKey(ArrayList<String> userIds, ArrayList<PGPSecretKey> keys,
|
||||
ArrayList<Integer> keysUsages, ArrayList<GregorianCalendar> keysExpiryDates,
|
||||
long masterKeyId, String oldPassPhrase,
|
||||
PGPPublicKey oldPublicKey, String oldPassPhrase,
|
||||
String newPassPhrase) throws PgpGeneralException, NoSuchProviderException,
|
||||
PGPException, NoSuchAlgorithmException, SignatureException, IOException {
|
||||
|
||||
@ -215,131 +218,166 @@ public class PgpKeyOperation {
|
||||
|
||||
updateProgress(R.string.progress_preparing_master_key, 10, 100);
|
||||
|
||||
int usageId = keysUsages.get(0);
|
||||
boolean canSign =
|
||||
(usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
boolean canEncrypt =
|
||||
(usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
// prepare keyring generator with given master public and secret key
|
||||
PGPKeyRingGenerator keyGen;
|
||||
PGPPublicKey masterPublicKey; {
|
||||
|
||||
String mainUserId = userIds.get(0);
|
||||
String mainUserId = userIds.get(0);
|
||||
|
||||
PGPSecretKey masterKey = keys.get(0);
|
||||
// prepare the master key pair
|
||||
PGPKeyPair masterKeyPair; {
|
||||
|
||||
// this removes all userIds and certifications previously attached to the masterPublicKey
|
||||
PGPPublicKey tmpKey = masterKey.getPublicKey();
|
||||
PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
|
||||
tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
|
||||
PGPSecretKey masterKey = keys.get(0);
|
||||
|
||||
// already done by code above:
|
||||
// PGPPublicKey masterPublicKey = masterKey.getPublicKey();
|
||||
// // Somehow, the PGPPublicKey already has an empty certification attached to it when the
|
||||
// // keyRing is generated the first time, we remove that when it exists, before adding the
|
||||
// new
|
||||
// // ones
|
||||
// PGPPublicKey masterPublicKeyRmCert = PGPPublicKey.removeCertification(masterPublicKey,
|
||||
// "");
|
||||
// if (masterPublicKeyRmCert != null) {
|
||||
// masterPublicKey = masterPublicKeyRmCert;
|
||||
// }
|
||||
// this removes all userIds and certifications previously attached to the masterPublicKey
|
||||
PGPPublicKey tmpKey = masterKey.getPublicKey();
|
||||
masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
|
||||
tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
|
||||
PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
|
||||
// already done by code above:
|
||||
// PGPPublicKey masterPublicKey = masterKey.getPublicKey();
|
||||
// // Somehow, the PGPPublicKey already has an empty certification attached to it when the
|
||||
// // keyRing is generated the first time, we remove that when it exists, before adding the
|
||||
// new
|
||||
// // ones
|
||||
// PGPPublicKey masterPublicKeyRmCert = PGPPublicKey.removeCertification(masterPublicKey,
|
||||
// "");
|
||||
// if (masterPublicKeyRmCert != null) {
|
||||
// masterPublicKey = masterPublicKeyRmCert;
|
||||
// }
|
||||
|
||||
updateProgress(R.string.progress_certifying_master_key, 20, 100);
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
|
||||
PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
|
||||
|
||||
// TODO: if we are editing a key, keep old certs, don't remake certs we don't have to.
|
||||
updateProgress(R.string.progress_certifying_master_key, 20, 100);
|
||||
|
||||
for (String userId : userIds) {
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
// re-add old certificates, or create new ones for new uids
|
||||
for (String userId : userIds) {
|
||||
// re-add certs for this uid, take a note if self-signed cert is in there
|
||||
boolean foundSelfSign = false;
|
||||
Iterator<PGPSignature> it = tmpKey.getSignaturesForID(userId);
|
||||
if(it != null) for(PGPSignature sig : new IterableIterator<PGPSignature>(it)) {
|
||||
if(sig.getKeyID() == masterPublicKey.getKeyID()) {
|
||||
// already have a self sign? skip this other one, then.
|
||||
// note: PGPKeyRingGenerator adds one cert for the main user id, which
|
||||
// will lead to duplicates. unfortunately, if we add any other here
|
||||
// first, that will change the main user id order...
|
||||
if(foundSelfSign)
|
||||
continue;
|
||||
foundSelfSign = true;
|
||||
}
|
||||
Log.d(Constants.TAG, "adding old sig for " + userId + " from "
|
||||
+ PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()));
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, sig);
|
||||
}
|
||||
|
||||
sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
|
||||
// there was an old self-signed certificate for this uid
|
||||
if(foundSelfSign)
|
||||
continue;
|
||||
|
||||
PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
|
||||
Log.d(Constants.TAG, "generating self-signed cert for " + userId);
|
||||
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
|
||||
}
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
|
||||
PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
|
||||
sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
|
||||
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
|
||||
|
||||
int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
|
||||
if (canEncrypt) {
|
||||
keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
|
||||
}
|
||||
hashedPacketsGen.setKeyFlags(true, keyFlags);
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
|
||||
}
|
||||
|
||||
hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
|
||||
hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
|
||||
hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
|
||||
|
||||
if (keysExpiryDates.get(0) != null) {
|
||||
GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||
creationDate.setTime(masterPublicKey.getCreationTime());
|
||||
GregorianCalendar expiryDate = keysExpiryDates.get(0);
|
||||
//note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c
|
||||
//here we purposefully ignore partial days in each date - long type has no fractional part!
|
||||
long numDays =
|
||||
(expiryDate.getTimeInMillis() / 86400000) - (creationDate.getTimeInMillis() / 86400000);
|
||||
if (numDays <= 0) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_expiry_must_come_after_creation));
|
||||
masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
|
||||
}
|
||||
hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400);
|
||||
} else {
|
||||
//do this explicitly, although since we're rebuilding,
|
||||
hashedPacketsGen.setKeyExpirationTime(false, 0);
|
||||
//this happens anyway
|
||||
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen;
|
||||
PGPSignatureSubpacketGenerator unhashedPacketsGen; {
|
||||
|
||||
hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
|
||||
int usageId = keysUsages.get(0);
|
||||
boolean canEncrypt =
|
||||
(usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
|
||||
int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
|
||||
if (canEncrypt) {
|
||||
keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
|
||||
}
|
||||
hashedPacketsGen.setKeyFlags(true, keyFlags);
|
||||
|
||||
hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
|
||||
hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
|
||||
hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
|
||||
|
||||
if (keysExpiryDates.get(0) != null) {
|
||||
GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||
creationDate.setTime(masterPublicKey.getCreationTime());
|
||||
GregorianCalendar expiryDate = keysExpiryDates.get(0);
|
||||
//note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c
|
||||
//here we purposefully ignore partial days in each date - long type has no fractional part!
|
||||
long numDays =
|
||||
(expiryDate.getTimeInMillis() / 86400000) - (creationDate.getTimeInMillis() / 86400000);
|
||||
if (numDays <= 0) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_expiry_must_come_after_creation));
|
||||
}
|
||||
hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400);
|
||||
} else {
|
||||
//do this explicitly, although since we're rebuilding,
|
||||
hashedPacketsGen.setKeyExpirationTime(false, 0);
|
||||
//this happens anyway
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(R.string.progress_building_master_key, 30, 100);
|
||||
|
||||
// define hashing and signing algos
|
||||
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
|
||||
HashAlgorithmTags.SHA1);
|
||||
PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
|
||||
|
||||
// Build key encrypter based on passphrase
|
||||
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
|
||||
PGPEncryptedData.CAST5, sha1Calc)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||
newPassPhrase.toCharArray());
|
||||
|
||||
keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
|
||||
masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
|
||||
unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
|
||||
|
||||
}
|
||||
|
||||
updateProgress(R.string.progress_building_master_key, 30, 100);
|
||||
|
||||
// define hashing and signing algos
|
||||
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
|
||||
HashAlgorithmTags.SHA1);
|
||||
PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
|
||||
|
||||
// Build key encrypter based on passphrase
|
||||
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
|
||||
PGPEncryptedData.CAST5, sha1Calc)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||
newPassPhrase.toCharArray());
|
||||
|
||||
PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
|
||||
masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
|
||||
unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
|
||||
|
||||
updateProgress(R.string.progress_adding_sub_keys, 40, 100);
|
||||
|
||||
for (int i = 1; i < keys.size(); ++i) {
|
||||
updateProgress(40 + 50 * (i - 1) / (keys.size() - 1), 100);
|
||||
updateProgress(40 + 40 * (i - 1) / (keys.size() - 1), 100);
|
||||
|
||||
PGPSecretKey subKey = keys.get(i);
|
||||
PGPPublicKey subPublicKey = subKey.getPublicKey();
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||
oldPassPhrase.toCharArray());
|
||||
PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2);
|
||||
PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor);
|
||||
|
||||
// TODO: now used without algorithm and creation time?! (APG 1)
|
||||
PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey);
|
||||
|
||||
hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
|
||||
keyFlags = 0;
|
||||
int keyFlags = 0;
|
||||
|
||||
usageId = keysUsages.get(i);
|
||||
canSign =
|
||||
int usageId = keysUsages.get(i);
|
||||
boolean canSign =
|
||||
(usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
canEncrypt =
|
||||
boolean canEncrypt =
|
||||
(usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
if (canSign) {
|
||||
Date todayDate = new Date(); //both sig times the same
|
||||
@ -388,53 +426,99 @@ public class PgpKeyOperation {
|
||||
PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
|
||||
PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
|
||||
|
||||
updateProgress(R.string.progress_re_adding_certs, 80, 100);
|
||||
|
||||
// re-add certificates from old public key
|
||||
// TODO: this only takes care of user id certificates, what about others?
|
||||
PGPPublicKey pubkey = publicKeyRing.getPublicKey();
|
||||
for(String uid : new IterableIterator<String>(pubkey.getUserIDs())) {
|
||||
for(PGPSignature sig : new IterableIterator<PGPSignature>(oldPublicKey.getSignaturesForID(uid))) {
|
||||
// but skip self certificates
|
||||
if(sig.getKeyID() == pubkey.getKeyID())
|
||||
continue;
|
||||
pubkey = PGPPublicKey.addCertification(pubkey, uid, sig);
|
||||
}
|
||||
}
|
||||
publicKeyRing = PGPPublicKeyRing.insertPublicKey(publicKeyRing, pubkey);
|
||||
|
||||
updateProgress(R.string.progress_saving_key_ring, 90, 100);
|
||||
|
||||
/* additional handy debug info
|
||||
Log.d(Constants.TAG, " ------- in private key -------");
|
||||
for(String uid : new IterableIterator<String>(secretKeyRing.getPublicKey().getUserIDs())) {
|
||||
for(PGPSignature sig : new IterableIterator<PGPSignature>(secretKeyRing.getPublicKey().getSignaturesForID(uid))) {
|
||||
Log.d(Constants.TAG, "sig: " + PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()) + " for " + uid);
|
||||
}
|
||||
}
|
||||
Log.d(Constants.TAG, " ------- in public key -------");
|
||||
for(String uid : new IterableIterator<String>(publicKeyRing.getPublicKey().getUserIDs())) {
|
||||
for(PGPSignature sig : new IterableIterator<PGPSignature>(publicKeyRing.getPublicKey().getSignaturesForID(uid))) {
|
||||
Log.d(Constants.TAG, "sig: " + PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()) + " for " + uid);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
ProviderHelper.saveKeyRing(mContext, secretKeyRing);
|
||||
ProviderHelper.saveKeyRing(mContext, publicKeyRing);
|
||||
|
||||
updateProgress(R.string.progress_done, 100, 100);
|
||||
}
|
||||
|
||||
public PGPPublicKeyRing certifyKey(long masterKeyId, long pubKeyId, String passphrase)
|
||||
/**
|
||||
* Certify the given pubkeyid with the given masterkeyid.
|
||||
*
|
||||
* @param masterKeyId Certifying key, must be available as secret key
|
||||
* @param pubKeyId ID of public key to certify
|
||||
* @param userIds User IDs to certify, must not be null or empty
|
||||
* @param passphrase Passphrase of the secret key
|
||||
* @return A keyring with added certifications
|
||||
*/
|
||||
public PGPPublicKeyRing certifyKey(long masterKeyId, long pubKeyId, List<String> userIds, String passphrase)
|
||||
throws PgpGeneralException, NoSuchAlgorithmException, NoSuchProviderException,
|
||||
PGPException, SignatureException {
|
||||
if (passphrase == null) {
|
||||
throw new PgpGeneralException("Unable to obtain passphrase");
|
||||
} else {
|
||||
|
||||
// create a signatureGenerator from the supplied masterKeyId and passphrase
|
||||
PGPSignatureGenerator signatureGenerator; {
|
||||
|
||||
PGPSecretKey certificationKey = PgpKeyHelper.getCertificationKey(mContext, masterKeyId);
|
||||
if (certificationKey == null) {
|
||||
throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
|
||||
}
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
|
||||
PGPPrivateKey signaturePrivateKey = certificationKey.extractPrivateKey(keyDecryptor);
|
||||
if (signaturePrivateKey == null) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_could_not_extract_private_key));
|
||||
}
|
||||
|
||||
// TODO: SHA256 fixed?
|
||||
JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
|
||||
certificationKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
|
||||
signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
|
||||
signatureGenerator.init(PGPSignature.DEFAULT_CERTIFICATION, signaturePrivateKey);
|
||||
}
|
||||
|
||||
{ // supply signatureGenerator with a SubpacketVector
|
||||
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
|
||||
PGPSignatureSubpacketVector packetVector = spGen.generate();
|
||||
signatureGenerator.setHashedSubpackets(packetVector);
|
||||
}
|
||||
|
||||
// fetch public key ring, add the certification and return it
|
||||
PGPPublicKeyRing pubring = ProviderHelper
|
||||
.getPGPPublicKeyRingByKeyId(mContext, pubKeyId);
|
||||
|
||||
PGPSecretKey certificationKey = PgpKeyHelper.getCertificationKey(mContext, masterKeyId);
|
||||
if (certificationKey == null) {
|
||||
throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
|
||||
PGPPublicKey signedKey = pubring.getPublicKey(pubKeyId);
|
||||
for(String userId : new IterableIterator<String>(userIds.iterator())) {
|
||||
PGPSignature sig = signatureGenerator.generateCertification(userId, signedKey);
|
||||
signedKey = PGPPublicKey.addCertification(signedKey, userId, sig);
|
||||
}
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
|
||||
PGPPrivateKey signaturePrivateKey = certificationKey.extractPrivateKey(keyDecryptor);
|
||||
if (signaturePrivateKey == null) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_could_not_extract_private_key));
|
||||
}
|
||||
|
||||
// TODO: SHA256 fixed?
|
||||
JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
|
||||
certificationKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
|
||||
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
|
||||
contentSignerBuilder);
|
||||
|
||||
signatureGenerator.init(PGPSignature.DIRECT_KEY, signaturePrivateKey);
|
||||
|
||||
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
|
||||
|
||||
PGPSignatureSubpacketVector packetVector = spGen.generate();
|
||||
signatureGenerator.setHashedSubpackets(packetVector);
|
||||
|
||||
PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId),
|
||||
signatureGenerator.generate());
|
||||
pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
|
||||
|
||||
return pubring;
|
||||
|
@ -55,6 +55,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
|
||||
private static final int PUBLIC_KEY_RING_USER_ID = 121;
|
||||
private static final int PUBLIC_KEY_RING_USER_ID_BY_ROW_ID = 122;
|
||||
private static final int PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID = 123;
|
||||
|
||||
private static final int SECRET_KEY_RING = 201;
|
||||
private static final int SECRET_KEY_RING_BY_ROW_ID = 202;
|
||||
@ -157,6 +158,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
* <pre>
|
||||
* key_rings/public/#/user_ids
|
||||
* key_rings/public/#/user_ids/#
|
||||
* key_rings/public/master_key_id/#/user_ids
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
@ -165,6 +167,10 @@ public class KeychainProvider extends ContentProvider {
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_USER_IDS + "/#",
|
||||
PUBLIC_KEY_RING_USER_ID_BY_ROW_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/"
|
||||
+ KeychainContract.PATH_BY_MASTER_KEY_ID + "/*/" + KeychainContract.PATH_USER_IDS,
|
||||
PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID);
|
||||
|
||||
/**
|
||||
* secret key rings
|
||||
@ -311,6 +317,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
return Keys.CONTENT_ITEM_TYPE;
|
||||
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID:
|
||||
case SECRET_KEY_RING_USER_ID:
|
||||
return UserIds.CONTENT_TYPE;
|
||||
|
||||
@ -348,6 +355,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
case PUBLIC_KEY_RING_KEY:
|
||||
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID:
|
||||
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
type = KeyTypes.PUBLIC;
|
||||
break;
|
||||
@ -390,6 +398,11 @@ public class KeychainProvider extends ContentProvider {
|
||||
// TODO: deprecated master key id
|
||||
//projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEYS + "." + KeysColumns.KEY_ID);
|
||||
|
||||
projectionMap.put(KeysColumns.ALGORITHM, Tables.KEYS + "." + KeysColumns.ALGORITHM);
|
||||
projectionMap.put(KeysColumns.KEY_SIZE, Tables.KEYS + "." + KeysColumns.KEY_SIZE);
|
||||
projectionMap.put(KeysColumns.CREATION, Tables.KEYS + "." + KeysColumns.CREATION);
|
||||
projectionMap.put(KeysColumns.EXPIRY, Tables.KEYS + "." + KeysColumns.EXPIRY);
|
||||
projectionMap.put(KeysColumns.KEY_RING_ROW_ID, Tables.KEYS + "." + KeysColumns.KEY_RING_ROW_ID);
|
||||
projectionMap.put(KeysColumns.FINGERPRINT, Tables.KEYS + "." + KeysColumns.FINGERPRINT);
|
||||
projectionMap.put(KeysColumns.IS_REVOKED, Tables.KEYS + "." + KeysColumns.IS_REVOKED);
|
||||
|
||||
@ -429,6 +442,18 @@ public class KeychainProvider extends ContentProvider {
|
||||
return projectionMap;
|
||||
}
|
||||
|
||||
private HashMap<String, String> getProjectionMapForUserIds() {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
|
||||
projectionMap.put(BaseColumns._ID, Tables.USER_IDS + "." + BaseColumns._ID);
|
||||
projectionMap.put(UserIdsColumns.USER_ID, Tables.USER_IDS + "." + UserIdsColumns.USER_ID);
|
||||
projectionMap.put(UserIdsColumns.RANK, Tables.USER_IDS + "." + UserIdsColumns.RANK);
|
||||
projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "."
|
||||
+ KeyRingsColumns.MASTER_KEY_ID);
|
||||
|
||||
return projectionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds default query for keyRings: KeyRings table is joined with UserIds and Keys
|
||||
*/
|
||||
@ -633,6 +658,17 @@ public class KeychainProvider extends ContentProvider {
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID:
|
||||
qb.setTables(Tables.USER_IDS + " INNER JOIN " + Tables.KEY_RINGS + " ON " + "("
|
||||
+ Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "."
|
||||
+ KeysColumns.KEY_RING_ROW_ID + " )");
|
||||
qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(3));
|
||||
|
||||
qb.setProjectionMap(getProjectionMapForUserIds());
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
case SECRET_KEY_RING_USER_ID:
|
||||
qb.setTables(Tables.USER_IDS
|
||||
|
@ -143,6 +143,7 @@ public class KeychainIntentService extends IntentService
|
||||
// sign key
|
||||
public static final String CERTIFY_KEY_MASTER_KEY_ID = "sign_key_master_key_id";
|
||||
public static final String CERTIFY_KEY_PUB_KEY_ID = "sign_key_pub_key_id";
|
||||
public static final String CERTIFY_KEY_UIDS = "sign_key_uids";
|
||||
|
||||
/*
|
||||
* possible data keys as result send over messenger
|
||||
@ -542,8 +543,9 @@ public class KeychainIntentService extends IntentService
|
||||
ProviderHelper.getPGPSecretKeyRingByKeyId(this, masterKeyId),
|
||||
oldPassPhrase, newPassPhrase);
|
||||
} else {
|
||||
keyOperations.buildSecretKey(userIds, keys, keysUsages, keysExpiryDates, masterKeyId,
|
||||
oldPassPhrase, newPassPhrase);
|
||||
PGPPublicKey pubkey = ProviderHelper.getPGPPublicKeyByKeyId(this, masterKeyId);
|
||||
keyOperations.buildSecretKey(userIds, keys, keysUsages, keysExpiryDates,
|
||||
pubkey, oldPassPhrase, newPassPhrase);
|
||||
}
|
||||
PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase);
|
||||
|
||||
@ -804,6 +806,7 @@ public class KeychainIntentService extends IntentService
|
||||
/* Input */
|
||||
long masterKeyId = data.getLong(CERTIFY_KEY_MASTER_KEY_ID);
|
||||
long pubKeyId = data.getLong(CERTIFY_KEY_PUB_KEY_ID);
|
||||
ArrayList<String> userIds = data.getStringArrayList(CERTIFY_KEY_UIDS);
|
||||
|
||||
/* Operation */
|
||||
String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this,
|
||||
@ -811,7 +814,7 @@ public class KeychainIntentService extends IntentService
|
||||
|
||||
PgpKeyOperation keyOperation = new PgpKeyOperation(this, this);
|
||||
PGPPublicKeyRing signedPubKeyRing = keyOperation.certifyKey(masterKeyId, pubKeyId,
|
||||
signaturePassPhrase);
|
||||
userIds, signaturePassPhrase);
|
||||
|
||||
// store the signed key in our local cache
|
||||
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
|
||||
|
@ -19,11 +19,15 @@ package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.ActionBarActivity;
|
||||
import android.view.View;
|
||||
@ -32,26 +36,28 @@ import android.widget.*;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSignature;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.OtherHelper;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Signs the specified public key with the specified secret master key
|
||||
*/
|
||||
public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback {
|
||||
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
private BootstrapButton mSignButton;
|
||||
private CheckBox mUploadKeyCheckbox;
|
||||
private Spinner mSelectKeyserverSpinner;
|
||||
@ -62,6 +68,12 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
private long mPubKeyId = 0;
|
||||
private long mMasterKeyId = 0;
|
||||
|
||||
private ListView mUserIds;
|
||||
private ViewKeyUserIdsAdapter mUserIdsAdapter;
|
||||
|
||||
private static final int LOADER_ID_KEYRING = 0;
|
||||
private static final int LOADER_ID_USER_IDS = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -125,9 +137,18 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
Log.e(Constants.TAG, "uri: " + mDataUri);
|
||||
|
||||
PGPPublicKeyRing signKey = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, mDataUri);
|
||||
|
||||
mUserIds = (ListView) findViewById(R.id.user_ids);
|
||||
|
||||
mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0, true);
|
||||
mUserIds.setAdapter(mUserIdsAdapter);
|
||||
|
||||
getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this);
|
||||
getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
|
||||
|
||||
if (signKey != null) {
|
||||
mPubKeyId = PgpKeyHelper.getMasterKey(signKey).getKeyID();
|
||||
}
|
||||
@ -138,6 +159,76 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
}
|
||||
}
|
||||
|
||||
static final String[] KEYRING_PROJECTION =
|
||||
new String[] {
|
||||
KeychainContract.KeyRings._ID,
|
||||
KeychainContract.KeyRings.MASTER_KEY_ID,
|
||||
KeychainContract.Keys.FINGERPRINT,
|
||||
KeychainContract.UserIds.USER_ID
|
||||
};
|
||||
static final int INDEX_MASTER_KEY_ID = 1;
|
||||
static final int INDEX_FINGERPRINT = 2;
|
||||
static final int INDEX_USER_ID = 3;
|
||||
|
||||
static final String[] USER_IDS_PROJECTION =
|
||||
new String[]{
|
||||
KeychainContract.UserIds._ID,
|
||||
KeychainContract.UserIds.USER_ID,
|
||||
KeychainContract.UserIds.RANK
|
||||
};
|
||||
static final String USER_IDS_SORT_ORDER =
|
||||
KeychainContract.UserIds.RANK + " ASC";
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
switch(id) {
|
||||
case LOADER_ID_KEYRING:
|
||||
return new CursorLoader(this, mDataUri, KEYRING_PROJECTION, null, null, null);
|
||||
case LOADER_ID_USER_IDS: {
|
||||
Uri baseUri = KeychainContract.UserIds.buildUserIdsUri(mDataUri);
|
||||
return new CursorLoader(this, baseUri, USER_IDS_PROJECTION, null, null, USER_IDS_SORT_ORDER);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
switch(loader.getId()) {
|
||||
case LOADER_ID_KEYRING:
|
||||
// the first key here is our master key
|
||||
if (data.moveToFirst()) {
|
||||
long keyId = data.getLong(INDEX_MASTER_KEY_ID);
|
||||
String keyIdStr = PgpKeyHelper.convertKeyIdToHex(keyId);
|
||||
((TextView) findViewById(R.id.key_id)).setText(keyIdStr);
|
||||
|
||||
String mainUserId = data.getString(INDEX_USER_ID);
|
||||
((TextView) findViewById(R.id.main_user_id)).setText(mainUserId);
|
||||
|
||||
byte[] fingerprintBlob = data.getBlob(INDEX_FINGERPRINT);
|
||||
if (fingerprintBlob == null) {
|
||||
// FALLBACK for old database entries
|
||||
fingerprintBlob = ProviderHelper.getFingerprint(this, mDataUri);
|
||||
}
|
||||
String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob, true);
|
||||
((TextView) findViewById(R.id.fingerprint)).setText(OtherHelper.colorizeFingerprint(fingerprint));
|
||||
}
|
||||
break;
|
||||
case LOADER_ID_USER_IDS:
|
||||
mUserIdsAdapter.swapCursor(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
switch(loader.getId()) {
|
||||
case LOADER_ID_USER_IDS:
|
||||
mUserIdsAdapter.swapCursor(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void showPassphraseDialog(final long secretKeyId) {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@ -173,6 +264,7 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
// if we have already signed this key, dont bother doing it again
|
||||
boolean alreadySigned = false;
|
||||
|
||||
/* todo: reconsider this at a later point when certs are in the db
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSignature> itr = pubring.getPublicKey(mPubKeyId).getSignatures();
|
||||
while (itr.hasNext()) {
|
||||
@ -182,6 +274,7 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (!alreadySigned) {
|
||||
/*
|
||||
@ -209,6 +302,15 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
* kicks off the actual signing process on a background thread
|
||||
*/
|
||||
private void startSigning() {
|
||||
|
||||
// Bail out if there is not at least one user id selected
|
||||
ArrayList<String> userIds = mUserIdsAdapter.getSelectedUserIds();
|
||||
if(userIds.isEmpty()) {
|
||||
Toast.makeText(CertifyKeyActivity.this, "No User IDs to sign selected!",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send all information needed to service to sign key in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
@ -219,6 +321,7 @@ public class CertifyKeyActivity extends ActionBarActivity implements
|
||||
|
||||
data.putLong(KeychainIntentService.CERTIFY_KEY_MASTER_KEY_ID, mMasterKeyId);
|
||||
data.putLong(KeychainIntentService.CERTIFY_KEY_PUB_KEY_ID, mPubKeyId);
|
||||
data.putStringArrayList(KeychainIntentService.CERTIFY_KEY_UIDS, userIds);
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
|
@ -39,6 +39,7 @@ public class SelectSecretKeyLayoutFragment extends Fragment {
|
||||
private TextView mKeyUserId;
|
||||
private TextView mKeyUserIdRest;
|
||||
private TextView mKeyMasterKeyIdHex;
|
||||
private TextView mNoKeySelected;
|
||||
private BootstrapButton mSelectKeyButton;
|
||||
private Boolean mFilterCertify;
|
||||
|
||||
@ -60,10 +61,10 @@ public class SelectSecretKeyLayoutFragment extends Fragment {
|
||||
|
||||
public void selectKey(long secretKeyId) {
|
||||
if (secretKeyId == Id.key.none) {
|
||||
mKeyMasterKeyIdHex.setText(R.string.api_settings_no_key);
|
||||
mKeyUserIdRest.setText("");
|
||||
mNoKeySelected.setVisibility(View.VISIBLE);
|
||||
mKeyUserId.setVisibility(View.GONE);
|
||||
mKeyUserIdRest.setVisibility(View.GONE);
|
||||
mKeyMasterKeyIdHex.setVisibility(View.GONE);
|
||||
|
||||
} else {
|
||||
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
|
||||
@ -93,20 +94,25 @@ public class SelectSecretKeyLayoutFragment extends Fragment {
|
||||
mKeyMasterKeyIdHex.setText(masterkeyIdHex);
|
||||
mKeyUserId.setText(userName);
|
||||
mKeyUserIdRest.setText(userEmail);
|
||||
mKeyMasterKeyIdHex.setVisibility(View.VISIBLE);
|
||||
mKeyUserId.setVisibility(View.VISIBLE);
|
||||
mKeyUserIdRest.setVisibility(View.VISIBLE);
|
||||
mNoKeySelected.setVisibility(View.GONE);
|
||||
} else {
|
||||
mKeyMasterKeyIdHex.setText(getActivity().getResources().getString(R.string.no_key));
|
||||
mKeyMasterKeyIdHex.setVisibility(View.GONE);
|
||||
mKeyUserId.setVisibility(View.GONE);
|
||||
mKeyUserIdRest.setVisibility(View.GONE);
|
||||
mNoKeySelected.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
mKeyMasterKeyIdHex.setText(
|
||||
getActivity().getResources()
|
||||
.getString(R.string.no_keys_added_or_updated)
|
||||
+ " for master id: " + secretKeyId);
|
||||
mKeyMasterKeyIdHex.setVisibility(View.VISIBLE);
|
||||
mKeyUserId.setVisibility(View.GONE);
|
||||
mKeyUserIdRest.setVisibility(View.GONE);
|
||||
mNoKeySelected.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
@ -124,6 +130,7 @@ public class SelectSecretKeyLayoutFragment extends Fragment {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.select_secret_key_layout_fragment, container, false);
|
||||
|
||||
mNoKeySelected = (TextView) view.findViewById(R.id.no_key_selected);
|
||||
mKeyUserId = (TextView) view.findViewById(R.id.select_secret_key_user_id);
|
||||
mKeyUserIdRest = (TextView) view.findViewById(R.id.select_secret_key_user_id_rest);
|
||||
mKeyMasterKeyIdHex = (TextView) view.findViewById(R.id.select_secret_key_master_key_hex);
|
||||
|
@ -26,10 +26,7 @@ import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -135,7 +132,9 @@ public class ViewKeyMainFragment extends Fragment implements
|
||||
mSecretKey.setText(R.string.secret_key_yes);
|
||||
|
||||
// certify button
|
||||
mActionCertify.setVisibility(View.GONE);
|
||||
// TODO this button MIGHT be useful if the user wants to
|
||||
// certify a private key with another...
|
||||
// mActionCertify.setVisibility(View.GONE);
|
||||
|
||||
// edit button
|
||||
mActionEdit.setVisibility(View.VISIBLE);
|
||||
@ -156,17 +155,19 @@ public class ViewKeyMainFragment extends Fragment implements
|
||||
|
||||
// certify button
|
||||
mActionCertify.setVisibility(View.VISIBLE);
|
||||
mActionCertify.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View view) {
|
||||
certifyKey(KeychainContract.KeyRings.buildPublicKeyRingsByMasterKeyIdUri(
|
||||
Long.toString(masterKeyId)
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// edit button
|
||||
mActionEdit.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// TODO see todo note above, doing this here for now
|
||||
mActionCertify.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View view) {
|
||||
certifyKey(KeychainContract.KeyRings.buildPublicKeyRingsByMasterKeyIdUri(
|
||||
Long.toString(masterKeyId)
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
mActionEncrypt.setOnClickListener(new View.OnClickListener() {
|
||||
@ -198,13 +199,13 @@ public class ViewKeyMainFragment extends Fragment implements
|
||||
static final int KEYRING_INDEX_USER_ID = 2;
|
||||
|
||||
static final String[] USER_IDS_PROJECTION =
|
||||
new String[]{KeychainContract.UserIds._ID, KeychainContract.UserIds.USER_ID,
|
||||
KeychainContract.UserIds.RANK, };
|
||||
// not the main user id
|
||||
static final String USER_IDS_SELECTION =
|
||||
KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.RANK + " > 0 ";
|
||||
new String[]{
|
||||
KeychainContract.UserIds._ID,
|
||||
KeychainContract.UserIds.USER_ID,
|
||||
KeychainContract.UserIds.RANK,
|
||||
};
|
||||
static final String USER_IDS_SORT_ORDER =
|
||||
KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.USER_ID + " COLLATE LOCALIZED ASC";
|
||||
KeychainContract.UserIds.RANK + " COLLATE LOCALIZED ASC";
|
||||
|
||||
static final String[] KEYS_PROJECTION =
|
||||
new String[]{KeychainContract.Keys._ID, KeychainContract.Keys.KEY_ID,
|
||||
@ -240,7 +241,7 @@ public class ViewKeyMainFragment extends Fragment implements
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), baseUri, USER_IDS_PROJECTION, USER_IDS_SELECTION, null,
|
||||
return new CursorLoader(getActivity(), baseUri, USER_IDS_PROJECTION, null, null,
|
||||
USER_IDS_SORT_ORDER);
|
||||
}
|
||||
case LOADER_ID_KEYS: {
|
||||
@ -322,7 +323,7 @@ public class ViewKeyMainFragment extends Fragment implements
|
||||
}
|
||||
String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob, true);
|
||||
|
||||
mFingerprint.setText(colorizeFingerprint(fingerprint));
|
||||
mFingerprint.setText(OtherHelper.colorizeFingerprint(fingerprint));
|
||||
}
|
||||
|
||||
mKeysAdapter.swapCursor(data);
|
||||
@ -333,63 +334,6 @@ public class ViewKeyMainFragment extends Fragment implements
|
||||
}
|
||||
}
|
||||
|
||||
private SpannableStringBuilder colorizeFingerprint(String fingerprint) {
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder(fingerprint);
|
||||
try {
|
||||
// for each 4 characters of the fingerprint + 1 space
|
||||
for (int i = 0; i < fingerprint.length(); i += 5) {
|
||||
int spanEnd = Math.min(i + 4, fingerprint.length());
|
||||
String fourChars = fingerprint.substring(i, spanEnd);
|
||||
|
||||
int raw = Integer.parseInt(fourChars, 16);
|
||||
byte[] bytes = {(byte) ((raw >> 8) & 0xff - 128), (byte) (raw & 0xff - 128)};
|
||||
int[] color = OtherHelper.getRgbForData(bytes);
|
||||
int r = color[0];
|
||||
int g = color[1];
|
||||
int b = color[2];
|
||||
|
||||
// we cannot change black by multiplication, so adjust it to an almost-black grey,
|
||||
// which will then be brightened to the minimal brightness level
|
||||
if (r == 0 && g == 0 && b == 0) {
|
||||
r = 1;
|
||||
g = 1;
|
||||
b = 1;
|
||||
}
|
||||
|
||||
// Convert rgb to brightness
|
||||
double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
|
||||
// If a color is too dark to be seen on black,
|
||||
// then brighten it up to a minimal brightness.
|
||||
if (brightness < 80) {
|
||||
double factor = 80.0 / brightness;
|
||||
r = Math.min(255, (int) (r * factor));
|
||||
g = Math.min(255, (int) (g * factor));
|
||||
b = Math.min(255, (int) (b * factor));
|
||||
|
||||
// If it is too light, then darken it to a respective maximal brightness.
|
||||
} else if (brightness > 180) {
|
||||
double factor = 180.0 / brightness;
|
||||
r = (int) (r * factor);
|
||||
g = (int) (g * factor);
|
||||
b = (int) (b * factor);
|
||||
}
|
||||
|
||||
// Create a foreground color with the 3 digest integers as RGB
|
||||
// and then converting that int to hex to use as a color
|
||||
sb.setSpan(new ForegroundColorSpan(Color.rgb(r, g, b)),
|
||||
i, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "Colorization failed", e);
|
||||
// if anything goes wrong, then just display the fingerprint without colour,
|
||||
// instead of partially correct colour or wrong colours
|
||||
return new SpannableStringBuilder(fingerprint);
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the last Cursor provided to onLoadFinished() above is about to be closed.
|
||||
* We need to make sure we are no longer using it.
|
||||
|
@ -23,27 +23,49 @@ import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.TextView;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ViewKeyUserIdsAdapter extends CursorAdapter {
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
private int mIndexUserId;
|
||||
private int mIndexUserId, mIndexRank;
|
||||
private int mVerifiedId;
|
||||
|
||||
public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) {
|
||||
final private ArrayList<Boolean> mCheckStates;
|
||||
|
||||
public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags, boolean showCheckBoxes) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
|
||||
mCheckStates = showCheckBoxes ? new ArrayList<Boolean>() : null;
|
||||
|
||||
initIndex(c);
|
||||
}
|
||||
public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) {
|
||||
this(context, c, flags, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
initIndex(newCursor);
|
||||
if(mCheckStates != null) {
|
||||
mCheckStates.clear();
|
||||
if(newCursor != null) {
|
||||
int count = newCursor.getCount();
|
||||
mCheckStates.ensureCapacity(count);
|
||||
// initialize to true (use case knowledge: we usually want to sign all uids)
|
||||
for(int i = 0; i < count; i++)
|
||||
mCheckStates.add(true);
|
||||
}
|
||||
}
|
||||
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
@ -57,22 +79,69 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter {
|
||||
private void initIndex(Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
|
||||
mIndexRank = cursor.getColumnIndexOrThrow(UserIds.RANK);
|
||||
mVerifiedId = cursor.getColumnIndexOrThrow("verified");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
String userIdStr = cursor.getString(mIndexUserId);
|
||||
int verified = cursor.getInt(mVerifiedId);
|
||||
|
||||
TextView userId = (TextView) view.findViewById(R.id.userId);
|
||||
userId.setText(userIdStr + (verified > 0 ? " (ok)" : "(nope)"));
|
||||
TextView vRank = (TextView) view.findViewById(R.id.rank);
|
||||
TextView vUserId = (TextView) view.findViewById(R.id.userId);
|
||||
TextView vAddress = (TextView) view.findViewById(R.id.address);
|
||||
|
||||
vRank.setText(Integer.toString(cursor.getInt(mIndexRank)));
|
||||
|
||||
String[] userId = PgpKeyHelper.splitUserId(cursor.getString(mIndexUserId));
|
||||
int verified = cursor.getInt(mVerifiedId);
|
||||
if (userId[0] != null) {
|
||||
vUserId.setText(userId[0] + (verified > 0 ? " (ok)" : "(nope)"));
|
||||
} else {
|
||||
vUserId.setText(R.string.user_id_no_name);
|
||||
}
|
||||
vAddress.setText(userId[1]);
|
||||
|
||||
// don't care further if checkboxes aren't shown
|
||||
if(mCheckStates == null)
|
||||
return;
|
||||
|
||||
final CheckBox vCheckBox = (CheckBox) view.findViewById(R.id.checkBox);
|
||||
final int position = cursor.getPosition();
|
||||
vCheckBox.setClickable(false);
|
||||
vCheckBox.setChecked(mCheckStates.get(position));
|
||||
vCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
mCheckStates.set(position, b);
|
||||
}
|
||||
});
|
||||
view.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
vCheckBox.toggle();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public ArrayList<String> getSelectedUserIds() {
|
||||
ArrayList<String> result = new ArrayList<String>();
|
||||
for(int i = 0; i < mCheckStates.size(); i++) {
|
||||
if(mCheckStates.get(i)) {
|
||||
mCursor.moveToPosition(i);
|
||||
result.add(mCursor.getString(mIndexUserId));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.view_key_userids_item, null);
|
||||
View view = mInflater.inflate(R.layout.view_key_userids_item, null);
|
||||
// only need to do this once ever, since mShowCheckBoxes is final
|
||||
view.findViewById(R.id.checkBox).setVisibility(mCheckStates != null ? View.VISIBLE : View.GONE);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,6 +29,93 @@
|
||||
android:layout_marginTop="4dp"
|
||||
tools:layout="@layout/select_secret_key_layout_fragment" />
|
||||
|
||||
<TextView
|
||||
style="@style/SectionHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:text="KEY TO SIGN" />
|
||||
|
||||
<TableLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:shrinkColumns="1">
|
||||
|
||||
<TableRow
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingRight="10dip"
|
||||
android:text="@string/label_key_id" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/key_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingRight="5dip"
|
||||
android:text=""
|
||||
android:typeface="monospace" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingRight="10dip"
|
||||
android:text="@string/label_main_user_id" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_user_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:typeface="monospace" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingRight="10dip"
|
||||
android:text="@string/label_fingerprint" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fingerprint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:typeface="monospace" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
|
||||
<TextView
|
||||
style="@style/SectionHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:text="@string/section_uids_to_sign" />
|
||||
|
||||
<org.sufficientlysecure.keychain.ui.widget.FixedListView
|
||||
android:id="@+id/user_ids"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/SectionHeader"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -52,16 +52,25 @@
|
||||
android:layout_marginRight="5dip"
|
||||
android:text=""
|
||||
android:visibility="gone"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:paddingLeft="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/select_secret_key_master_key_hex"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:visibility="gone"
|
||||
android:layout_marginRight="15dip" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/no_key_selected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="@string/api_settings_no_key"
|
||||
android:layout_marginRight="5dip" />
|
||||
android:layout_marginTop="15dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -2,15 +2,44 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingRight="3dip"
|
||||
android:singleLine="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userId"
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/user_id"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
android:id="@+id/checkBox" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/rank"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="0"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userId"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/user_id_no_name"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/label_email"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:paddingLeft="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -465,6 +465,8 @@
|
||||
<string name="label_secret_key">Secret Key</string>
|
||||
<string name="secret_key_yes">available</string>
|
||||
<string name="secret_key_no">unavailable</string>
|
||||
|
||||
<!-- unsorted -->
|
||||
<string name="show_unknown_signatures">Show unknown signatures</string>
|
||||
<string name="section_signer_id">Signer</string>
|
||||
<string name="section_cert">Certificate Details</string>
|
||||
@ -476,5 +478,7 @@
|
||||
<string name="label_subkey_rank">Subkey Rank</string>
|
||||
<string name="unknown_uid"><![CDATA[<unknown>]]></string>
|
||||
<string name="empty_certs">No certificates for this key</string>
|
||||
<string name="section_uids_to_sign">User IDs to sign</string>
|
||||
<string name="progress_re_adding_certs">Reapplying certificates</string>
|
||||
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user