mirror of
https://github.com/moparisthebest/open-keychain
synced 2024-12-24 07:58:50 -05:00
Moving keytocard process into PgpKeyOperation.
This commit is contained in:
parent
a0107afd3e
commit
d21fb77336
@ -83,7 +83,7 @@ public class EditKeyOperation extends BaseOperation {
|
||||
CanonicalizedSecretKeyRing secRing =
|
||||
mProviderHelper.getCanonicalizedSecretKeyRing(saveParcel.mMasterKeyId);
|
||||
|
||||
modifyResult = keyOperations.modifySecretKeyRing(secRing, cryptoInput, saveParcel);
|
||||
modifyResult = keyOperations.modifySecretKeyRing(secRing, cryptoInput, saveParcel, log);
|
||||
if (modifyResult.isPending()) {
|
||||
return modifyResult;
|
||||
}
|
||||
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Joey Castillo <joey@joeycastillo.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.operations.results.NfcKeyToCardResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
|
||||
public class NfcKeyToCardOperation extends BaseOperation {
|
||||
public NfcKeyToCardOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
|
||||
super(context, providerHelper, progressable);
|
||||
}
|
||||
|
||||
public NfcKeyToCardResult execute(long subKeyId) {
|
||||
OperationResult.OperationLog log = new OperationResult.OperationLog();
|
||||
int indent = 0;
|
||||
long masterKeyId;
|
||||
|
||||
try {
|
||||
// fetch the indicated master key id
|
||||
masterKeyId = mProviderHelper.getMasterKeyId(subKeyId);
|
||||
CanonicalizedSecretKeyRing keyRing =
|
||||
mProviderHelper.getCanonicalizedSecretKeyRing(masterKeyId);
|
||||
|
||||
log.add(OperationResult.LogType.MSG_KC_SECRET, indent,
|
||||
KeyFormattingUtils.convertKeyIdToHex(masterKeyId));
|
||||
|
||||
// fetch the specific subkey
|
||||
CanonicalizedSecretKey subKey = keyRing.getSecretKey(subKeyId);
|
||||
|
||||
// Key algorithm must be RSA
|
||||
int algorithm = subKey.getAlgorithm();
|
||||
if (algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT &&
|
||||
algorithm != PublicKeyAlgorithmTags.RSA_SIGN &&
|
||||
algorithm != PublicKeyAlgorithmTags.RSA_GENERAL) {
|
||||
log.add(OperationResult.LogType.MSG_K2C_ERROR_BAD_ALGO, indent + 1);
|
||||
return new NfcKeyToCardResult(NfcKeyToCardResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
// Key size must be 2048
|
||||
int keySize = subKey.getBitStrength();
|
||||
if (keySize != 2048) {
|
||||
log.add(OperationResult.LogType.MSG_K2C_ERROR_BAD_SIZE, indent + 1);
|
||||
return new NfcKeyToCardResult(NfcKeyToCardResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
// Secret key parts must be available
|
||||
CanonicalizedSecretKey.SecretKeyType type = subKey.getSecretKeyType();
|
||||
if (type == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD ||
|
||||
type == CanonicalizedSecretKey.SecretKeyType.GNU_DUMMY) {
|
||||
log.add(OperationResult.LogType.MSG_K2C_ERROR_BAD_STRIPPED, indent + 1);
|
||||
return new NfcKeyToCardResult(NfcKeyToCardResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
if (type == CanonicalizedSecretKey.SecretKeyType.PIN ||
|
||||
type == CanonicalizedSecretKey.SecretKeyType.PATTERN ||
|
||||
type == CanonicalizedSecretKey.SecretKeyType.PASSPHRASE ||
|
||||
type == CanonicalizedSecretKey.SecretKeyType.PASSPHRASE_EMPTY) {
|
||||
log.add(OperationResult.LogType.MSG_PSE_PENDING_NFC, indent);
|
||||
return new NfcKeyToCardResult(log, RequiredInputParcel
|
||||
.createNfcKeyToCardOperation(masterKeyId, subKeyId));
|
||||
}
|
||||
|
||||
throw new AssertionError("Unhandled SecretKeyType! (should not happen)");
|
||||
} catch (ProviderHelper.NotFoundException e) {
|
||||
log.add(OperationResult.LogType.MSG_PSE_ERROR_UNLOCK, indent);
|
||||
return new NfcKeyToCardResult(NfcKeyToCardResult.RESULT_ERROR, log);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Joey Castillo <joey@joeycastillo.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.operations.results;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
|
||||
public class NfcKeyToCardResult extends InputPendingResult {
|
||||
public NfcKeyToCardResult(int result, OperationLog log) {
|
||||
super(result, log);
|
||||
}
|
||||
|
||||
public NfcKeyToCardResult(OperationLog log, RequiredInputParcel requiredInput) {
|
||||
super(log, requiredInput);
|
||||
}
|
||||
|
||||
public NfcKeyToCardResult(Parcel source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
}
|
||||
|
||||
public static Creator<NfcKeyToCardResult> CREATOR = new Creator<NfcKeyToCardResult>() {
|
||||
public NfcKeyToCardResult createFromParcel(final Parcel source) {
|
||||
return new NfcKeyToCardResult(source);
|
||||
}
|
||||
|
||||
public NfcKeyToCardResult[] newArray(final int size) {
|
||||
return new NfcKeyToCardResult[size];
|
||||
}
|
||||
};
|
||||
}
|
@ -494,6 +494,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_MF_ERROR_REVOKED_PRIMARY (LogLevel.ERROR, R.string.msg_mf_error_revoked_primary),
|
||||
MSG_MF_ERROR_SIG (LogLevel.ERROR, R.string.msg_mf_error_sig),
|
||||
MSG_MF_ERROR_SUBKEY_MISSING(LogLevel.ERROR, R.string.msg_mf_error_subkey_missing),
|
||||
MSG_MF_ERROR_CONFLICTING_NFC_COMMANDS(LogLevel.ERROR, R.string.msg_mf_error_conflicting_nfc_commands),
|
||||
MSG_MF_MASTER (LogLevel.DEBUG, R.string.msg_mf_master),
|
||||
MSG_MF_NOTATION_PIN (LogLevel.DEBUG, R.string.msg_mf_notation_pin),
|
||||
MSG_MF_NOTATION_EMPTY (LogLevel.DEBUG, R.string.msg_mf_notation_empty),
|
||||
@ -511,6 +512,8 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_MF_SUBKEY_NEW (LogLevel.INFO, R.string.msg_mf_subkey_new),
|
||||
MSG_MF_SUBKEY_REVOKE (LogLevel.INFO, R.string.msg_mf_subkey_revoke),
|
||||
MSG_MF_SUBKEY_STRIP (LogLevel.INFO, R.string.msg_mf_subkey_strip),
|
||||
MSG_MF_KEYTOCARD_START (LogLevel.INFO, R.string.msg_mf_keytocard_start),
|
||||
MSG_MF_KEYTOCARD_FINISH (LogLevel.OK, R.string.msg_mf_keytocard_finish),
|
||||
MSG_MF_SUCCESS (LogLevel.OK, R.string.msg_mf_success),
|
||||
MSG_MF_UID_ADD (LogLevel.INFO, R.string.msg_mf_uid_add),
|
||||
MSG_MF_UID_PRIMARY (LogLevel.INFO, R.string.msg_mf_uid_primary),
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.spongycastle.bcpg.S2K;
|
||||
import org.spongycastle.bcpg.sig.Features;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
@ -45,8 +46,10 @@ import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
|
||||
import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
|
||||
import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
@ -59,6 +62,7 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcKeyToCardOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@ -68,6 +72,7 @@ import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@ -356,10 +361,16 @@ public class PgpKeyOperation {
|
||||
*
|
||||
*/
|
||||
public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR,
|
||||
CryptoInputParcel cryptoInput,
|
||||
SaveKeyringParcel saveParcel) {
|
||||
CryptoInputParcel cryptoInput,
|
||||
SaveKeyringParcel saveParcel) {
|
||||
return modifySecretKeyRing(wsKR, cryptoInput, saveParcel, new OperationLog());
|
||||
}
|
||||
|
||||
public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR,
|
||||
CryptoInputParcel cryptoInput,
|
||||
SaveKeyringParcel saveParcel,
|
||||
OperationLog log) {
|
||||
|
||||
OperationLog log = new OperationLog();
|
||||
int indent = 0;
|
||||
|
||||
/*
|
||||
@ -400,6 +411,21 @@ public class PgpKeyOperation {
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
for(SaveKeyringParcel.SubkeyChange change : saveParcel.mChangeSubKeys) {
|
||||
if (change.mDummyDivert != null && change.mDummyDivert.length == 0) {
|
||||
// If this is a keytocard operation, see if it was completed: look for a hash
|
||||
// matching the given subkey ID in cryptoData.
|
||||
byte[] subKeyId = new byte[8];
|
||||
ByteBuffer buf = ByteBuffer.wrap(subKeyId);
|
||||
buf.putLong(change.mKeyId).rewind();
|
||||
|
||||
byte[] serialNumber = cryptoInput.getCryptoData().get(buf);
|
||||
if (serialNumber != null) {
|
||||
change.mDummyDivert = serialNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDummy(masterSecretKey) || saveParcel.isRestrictedOnly()) {
|
||||
log.add(LogType.MSG_MF_RESTRICTED_MODE, indent);
|
||||
return internalRestricted(sKR, saveParcel, log);
|
||||
@ -435,6 +461,8 @@ public class PgpKeyOperation {
|
||||
NfcSignOperationsBuilder nfcSignOps = new NfcSignOperationsBuilder(
|
||||
cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(),
|
||||
masterSecretKey.getKeyID());
|
||||
NfcKeyToCardOperationsBuilder nfcKeyToCardOps = new NfcKeyToCardOperationsBuilder(
|
||||
masterSecretKey.getKeyID());
|
||||
|
||||
progress(R.string.progress_modify, 0);
|
||||
|
||||
@ -743,22 +771,37 @@ public class PgpKeyOperation {
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
if (change.mDummyStrip || change.mDummyDivert != null) {
|
||||
if (change.mDummyStrip) {
|
||||
// IT'S DANGEROUS~
|
||||
// no really, it is. this operation irrevocably removes the private key data from the key
|
||||
if (change.mDummyStrip) {
|
||||
sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey());
|
||||
} else {
|
||||
// the serial number must be 16 bytes in length
|
||||
if (change.mDummyDivert.length != 16) {
|
||||
log.add(LogType.MSG_MF_ERROR_DIVERT_SERIAL,
|
||||
indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId));
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
}
|
||||
sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey());
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
|
||||
} else if (change.mDummyDivert != null) {
|
||||
if (change.mDummyDivert.length == 0) {
|
||||
// If serial number is 0 length, we're moving the key to a card.
|
||||
if (checkSmartCardCompatibility(sKey, log, indent + 1)) {
|
||||
log.add(LogType.MSG_MF_KEYTOCARD_START, indent + 1,
|
||||
KeyFormattingUtils.convertKeyIdToHex(change.mKeyId));
|
||||
nfcKeyToCardOps.addSubkey(change.mKeyId);
|
||||
} else {
|
||||
return new PgpEditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
} else if (change.mDummyDivert.length == 16) {
|
||||
// If serial number is 16 bytes long, we're associating the key with a card.
|
||||
log.add(LogType.MSG_MF_KEYTOCARD_FINISH, indent + 1,
|
||||
KeyFormattingUtils.convertKeyIdToHex(change.mKeyId),
|
||||
Hex.toHexString(change.mDummyDivert, 8, 6));
|
||||
sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey(), change.mDummyDivert);
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
|
||||
} else {
|
||||
log.add(LogType.MSG_MF_ERROR_DIVERT_SERIAL,
|
||||
indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId));
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// This doesn't concern us any further
|
||||
if (!change.mRecertify && (change.mExpiry == null && change.mFlags == null)) {
|
||||
continue;
|
||||
@ -980,11 +1023,21 @@ public class PgpKeyOperation {
|
||||
|
||||
progress(R.string.progress_done, 100);
|
||||
|
||||
if (!nfcSignOps.isEmpty() && !nfcKeyToCardOps.isEmpty()) {
|
||||
log.add(LogType.MSG_MF_ERROR_CONFLICTING_NFC_COMMANDS, indent+1);
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
if (!nfcSignOps.isEmpty()) {
|
||||
log.add(LogType.MSG_MF_REQUIRE_DIVERT, indent);
|
||||
return new PgpEditKeyResult(log, nfcSignOps.build());
|
||||
}
|
||||
|
||||
if (!nfcKeyToCardOps.isEmpty()) {
|
||||
log.add(LogType.MSG_MF_REQUIRE_DIVERT, indent);
|
||||
return new PgpEditKeyResult(log, nfcKeyToCardOps.build());
|
||||
}
|
||||
|
||||
log.add(LogType.MSG_MF_SUCCESS, indent);
|
||||
return new PgpEditKeyResult(OperationResult.RESULT_OK, log, new UncachedKeyRing(sKR));
|
||||
|
||||
@ -1042,6 +1095,9 @@ public class PgpKeyOperation {
|
||||
indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId));
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
log.add(LogType.MSG_MF_KEYTOCARD_FINISH, indent + 1,
|
||||
KeyFormattingUtils.convertKeyIdToHex(change.mKeyId),
|
||||
Hex.toHexString(change.mDummyDivert, 8, 6));
|
||||
sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey(), change.mDummyDivert);
|
||||
}
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
|
||||
@ -1481,4 +1537,29 @@ public class PgpKeyOperation {
|
||||
&& s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD;
|
||||
}
|
||||
|
||||
private static boolean checkSmartCardCompatibility(PGPSecretKey key, OperationLog log, int indent) {
|
||||
PGPPublicKey publicKey = key.getPublicKey();
|
||||
int algorithm = publicKey.getAlgorithm();
|
||||
if (algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT &&
|
||||
algorithm != PublicKeyAlgorithmTags.RSA_SIGN &&
|
||||
algorithm != PublicKeyAlgorithmTags.RSA_GENERAL) {
|
||||
log.add(OperationResult.LogType.MSG_K2C_ERROR_BAD_ALGO, indent + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Key size must be 2048
|
||||
int keySize = publicKey.getBitStrength();
|
||||
if (keySize != 2048) {
|
||||
log.add(OperationResult.LogType.MSG_K2C_ERROR_BAD_SIZE, indent + 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Secret key parts must be available
|
||||
if (isDivertToCard(key) || isDummy(key)) {
|
||||
log.add(OperationResult.LogType.MSG_K2C_ERROR_BAD_STRIPPED, indent + 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ import org.sufficientlysecure.keychain.operations.CertifyOperation;
|
||||
import org.sufficientlysecure.keychain.operations.DeleteOperation;
|
||||
import org.sufficientlysecure.keychain.operations.EditKeyOperation;
|
||||
import org.sufficientlysecure.keychain.operations.ImportExportOperation;
|
||||
import org.sufficientlysecure.keychain.operations.NfcKeyToCardOperation;
|
||||
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
|
||||
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
|
||||
import org.sufficientlysecure.keychain.operations.results.CertifyResult;
|
||||
@ -49,7 +48,6 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.DeleteResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.NfcKeyToCardResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult;
|
||||
@ -113,8 +111,6 @@ public class KeychainIntentService extends IntentService implements Progressable
|
||||
public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING";
|
||||
public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING";
|
||||
|
||||
public static final String ACTION_NFC_KEYTOCARD = Constants.INTENT_PREFIX + "NFC_KEYTOCARD";
|
||||
|
||||
public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING";
|
||||
|
||||
public static final String ACTION_CERTIFY_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING";
|
||||
@ -180,9 +176,6 @@ public class KeychainIntentService extends IntentService implements Progressable
|
||||
public static final String EXPORT_ALL = "export_all";
|
||||
public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id";
|
||||
|
||||
// NFC export key to card
|
||||
public static final String NFC_KEYTOCARD_SUBKEY_ID = "nfc_keytocard_subkey_id";
|
||||
|
||||
// upload key
|
||||
public static final String UPLOAD_KEY_SERVER = "upload_key_server";
|
||||
|
||||
@ -539,19 +532,6 @@ public class KeychainIntentService extends IntentService implements Progressable
|
||||
break;
|
||||
|
||||
}
|
||||
case ACTION_NFC_KEYTOCARD: {
|
||||
// Input
|
||||
long subKeyId = data.getLong(NFC_KEYTOCARD_SUBKEY_ID);
|
||||
|
||||
// Operation
|
||||
NfcKeyToCardOperation exportOp = new NfcKeyToCardOperation(this, providerHelper, this);
|
||||
NfcKeyToCardResult result = exportOp.execute(subKeyId);
|
||||
|
||||
// Result
|
||||
sendMessageToHandler(MessageStatus.OKAY, result);
|
||||
|
||||
break;
|
||||
}
|
||||
case ACTION_SIGN_ENCRYPT: {
|
||||
|
||||
// Input
|
||||
|
@ -95,7 +95,8 @@ public class SaveKeyringParcel implements Parcelable {
|
||||
}
|
||||
|
||||
for (SubkeyChange change : mChangeSubKeys) {
|
||||
if (change.mRecertify || change.mFlags != null || change.mExpiry != null) {
|
||||
if (change.mRecertify || change.mFlags != null || change.mExpiry != null ||
|
||||
(change.mDummyDivert != null && change.mDummyDivert.length == 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.sufficientlysecure.keychain.service.input;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
@ -7,8 +8,6 @@ import java.util.Date;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants.key;
|
||||
|
||||
|
||||
public class RequiredInputParcel implements Parcelable {
|
||||
|
||||
@ -87,11 +86,6 @@ public class RequiredInputParcel implements Parcelable {
|
||||
new byte[][] { inputHash }, null, null, null, subKeyId);
|
||||
}
|
||||
|
||||
public static RequiredInputParcel createNfcKeyToCardOperation(long masterKeyId, long subKeyId) {
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_KEYTOCARD, null, null, null,
|
||||
masterKeyId, subKeyId);
|
||||
}
|
||||
|
||||
public static RequiredInputParcel createRequiredSignPassphrase(
|
||||
long masterKeyId, long subKeyId, Date signatureTime) {
|
||||
return new RequiredInputParcel(RequiredInputType.PASSPHRASE,
|
||||
@ -216,4 +210,46 @@ public class RequiredInputParcel implements Parcelable {
|
||||
|
||||
}
|
||||
|
||||
public static class NfcKeyToCardOperationsBuilder {
|
||||
ArrayList<byte[]> mSubkeysToExport = new ArrayList<>();
|
||||
Long mMasterKeyId;
|
||||
|
||||
public NfcKeyToCardOperationsBuilder(Long masterKeyId) {
|
||||
mMasterKeyId = masterKeyId;
|
||||
}
|
||||
|
||||
public RequiredInputParcel build() {
|
||||
byte[][] inputHashes = new byte[mSubkeysToExport.size()][];
|
||||
mSubkeysToExport.toArray(inputHashes);
|
||||
ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0));
|
||||
|
||||
// We need to pass in a subkey here...
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_KEYTOCARD,
|
||||
inputHashes, null, null, mMasterKeyId, buf.getLong());
|
||||
}
|
||||
|
||||
public void addSubkey(long subkeyId) {
|
||||
byte[] subKeyId = new byte[8];
|
||||
ByteBuffer buf = ByteBuffer.wrap(subKeyId);
|
||||
buf.putLong(subkeyId).rewind();
|
||||
mSubkeysToExport.add(subKeyId);
|
||||
}
|
||||
|
||||
public void addAll(RequiredInputParcel input) {
|
||||
if (!mMasterKeyId.equals(input.mMasterKeyId)) {
|
||||
throw new AssertionError("Master keys must match, this is a programming error!");
|
||||
}
|
||||
if (input.mType != RequiredInputType.NFC_KEYTOCARD) {
|
||||
throw new AssertionError("Operation types must match, this is a programming error!");
|
||||
}
|
||||
|
||||
Collections.addAll(mSubkeysToExport, input.mInputHashes);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return mSubkeysToExport.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
@ -42,6 +43,7 @@ import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
@ -418,6 +420,13 @@ public class EditKeyFragment extends CryptoOperationFragment implements
|
||||
}
|
||||
break;
|
||||
case EditSubkeyDialogFragment.MESSAGE_STRIP: {
|
||||
CanonicalizedSecretKey.SecretKeyType secretKeyType =
|
||||
mSubkeysAdapter.getSecretKeyType(position);
|
||||
if (secretKeyType == CanonicalizedSecretKey.SecretKeyType.GNU_DUMMY) {
|
||||
// Key is already stripped; this is a no-op.
|
||||
break;
|
||||
}
|
||||
|
||||
SubkeyChange change = mSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
if (change == null) {
|
||||
mSaveKeyringParcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, null));
|
||||
@ -425,47 +434,39 @@ public class EditKeyFragment extends CryptoOperationFragment implements
|
||||
}
|
||||
// toggle
|
||||
change.mDummyStrip = !change.mDummyStrip;
|
||||
if (change.mDummyStrip && change.mDummyDivert != null) {
|
||||
// User had chosen to divert key, but now wants to strip it instead.
|
||||
change.mDummyDivert = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditSubkeyDialogFragment.MESSAGE_KEYTOCARD: {
|
||||
CanonicalizedSecretKey.SecretKeyType secretKeyType =
|
||||
mSubkeysAdapter.getSecretKeyType(position);
|
||||
if (secretKeyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD ||
|
||||
secretKeyType == CanonicalizedSecretKey.SecretKeyType.GNU_DUMMY) {
|
||||
Toast.makeText(EditKeyFragment.this.getActivity(),
|
||||
R.string.edit_key_error_bad_nfc_stripped, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
SubkeyChange change;
|
||||
change = mSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
if (change == null) {
|
||||
mSaveKeyringParcel.mChangeSubKeys.add(
|
||||
new SubkeyChange(keyId, false, new byte[0])
|
||||
new SubkeyChange(keyId, false, null)
|
||||
);
|
||||
change = mSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
}
|
||||
// toggle
|
||||
if (change.mDummyDivert == null) {
|
||||
change.mDummyDivert = new byte[0];
|
||||
// If user had chosen to strip key, we cancel that action now.
|
||||
change.mDummyStrip = false;
|
||||
} else {
|
||||
change.mDummyDivert = null;
|
||||
}
|
||||
final Bundle data = new Bundle();
|
||||
data.putLong(KeychainIntentService.NFC_KEYTOCARD_SUBKEY_ID, keyId);
|
||||
Intent intent = new Intent(EditKeyFragment.this.getActivity(),
|
||||
KeychainIntentService.class);
|
||||
intent.setAction(KeychainIntentService.ACTION_NFC_KEYTOCARD);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
ServiceProgressHandler serviceHandler = new ServiceProgressHandler(
|
||||
getActivity(),
|
||||
getString(R.string.progress_exporting),
|
||||
ProgressDialog.STYLE_HORIZONTAL,
|
||||
ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) {
|
||||
public void handleMessage(Message message) {
|
||||
super.handleMessage(message);
|
||||
if (EditKeyFragment.this.handlePendingMessage(message)) {
|
||||
return;
|
||||
}
|
||||
Bundle data = message.getData();
|
||||
OperationResult result = data.getParcelable(OperationResult.EXTRA_RESULT);
|
||||
if (result.getResult() == OperationResult.RESULT_ERROR) {
|
||||
result.createNotify(getActivity()).show();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(serviceHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
serviceHandler.showProgressDialog(getActivity());
|
||||
|
||||
getActivity().startService(intent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -637,18 +638,6 @@ public class EditKeyFragment extends CryptoOperationFragment implements
|
||||
Intent intent = new Intent(getActivity(), KeychainIntentService.class);
|
||||
intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING);
|
||||
|
||||
for (SubkeyChange change : mSaveKeyringParcel.mChangeSubKeys) {
|
||||
if(change.mDummyDivert != null) {
|
||||
// Convert long key ID to byte buffer
|
||||
byte[] subKeyId = new byte[8];
|
||||
ByteBuffer buf = ByteBuffer.wrap(subKeyId);
|
||||
buf.putLong(change.mKeyId).rewind();
|
||||
|
||||
byte[] cardSerial = cryptoInput.getCryptoData().get(buf);
|
||||
change.mDummyDivert = cardSerial;
|
||||
}
|
||||
}
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput);
|
||||
|
@ -23,6 +23,7 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -59,9 +60,7 @@ public class NfcOperationActivity extends BaseNfcActivity {
|
||||
mRequiredInput = data.getParcelable(EXTRA_REQUIRED_INPUT);
|
||||
mServiceIntent = data.getParcelable(EXTRA_SERVICE_INTENT);
|
||||
|
||||
if (mRequiredInput.mType == RequiredInputParcel.RequiredInputType.NFC_KEYTOCARD) {
|
||||
obtainKeyExportPassphrase(RequiredInputParcel.createRequiredPassphrase(mRequiredInput));
|
||||
} else {
|
||||
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_KEYTOCARD) {
|
||||
obtainYubiKeyPin(RequiredInputParcel.createRequiredPassphrase(mRequiredInput));
|
||||
}
|
||||
}
|
||||
@ -99,38 +98,51 @@ public class NfcOperationActivity extends BaseNfcActivity {
|
||||
CanonicalizedSecretKeyRing secretKeyRing;
|
||||
try {
|
||||
secretKeyRing = providerHelper.getCanonicalizedSecretKeyRing(
|
||||
KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mRequiredInput.getSubKeyId())
|
||||
KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mRequiredInput.getMasterKeyId())
|
||||
);
|
||||
} catch (ProviderHelper.NotFoundException e) {
|
||||
throw new IOException("Couldn't find subkey for key to card operation.");
|
||||
}
|
||||
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(mRequiredInput.getSubKeyId());
|
||||
|
||||
long keyGenerationTimestampMillis = key.getCreationTime().getTime();
|
||||
long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
|
||||
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
|
||||
byte[] cardSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
|
||||
// Note: we're abusing mInputHashes to hold the subkey IDs we need to export.
|
||||
for (int i = 0; i < mRequiredInput.mInputHashes.length; i++) {
|
||||
byte[] subkeyBytes = mRequiredInput.mInputHashes[i];
|
||||
ByteBuffer buf = ByteBuffer.wrap(subkeyBytes);
|
||||
long subkeyId = buf.getLong();
|
||||
|
||||
if (key.canSign() || key.canCertify()) {
|
||||
nfcPutKey(0xB6, key);
|
||||
nfcPutData(0xCE, timestampBytes);
|
||||
nfcPutData(0xC7, key.getFingerprint());
|
||||
} else if (key.canEncrypt()) {
|
||||
nfcPutKey(0xB8, key);
|
||||
nfcPutData(0xCF, timestampBytes);
|
||||
nfcPutData(0xC8, key.getFingerprint());
|
||||
} else if (key.canAuthenticate()) {
|
||||
nfcPutKey(0xA4, key);
|
||||
nfcPutData(0xD0, timestampBytes);
|
||||
nfcPutData(0xC9, key.getFingerprint());
|
||||
} else {
|
||||
throw new IOException("Inappropriate key flags for smart card key.");
|
||||
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
|
||||
|
||||
long keyGenerationTimestampMillis = key.getCreationTime().getTime();
|
||||
long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
|
||||
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
|
||||
byte[] cardSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
|
||||
|
||||
Passphrase passphrase;
|
||||
try {
|
||||
passphrase = PassphraseCacheService.getCachedPassphrase(this,
|
||||
mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
|
||||
} catch (PassphraseCacheService.KeyNotFoundException e) {
|
||||
throw new IOException("Unable to get cached passphrase!");
|
||||
}
|
||||
|
||||
if (key.canSign() || key.canCertify()) {
|
||||
nfcPutKey(0xB6, key, passphrase);
|
||||
nfcPutData(0xCE, timestampBytes);
|
||||
nfcPutData(0xC7, key.getFingerprint());
|
||||
} else if (key.canEncrypt()) {
|
||||
nfcPutKey(0xB8, key, passphrase);
|
||||
nfcPutData(0xCF, timestampBytes);
|
||||
nfcPutData(0xC8, key.getFingerprint());
|
||||
} else if (key.canAuthenticate()) {
|
||||
nfcPutKey(0xA4, key, passphrase);
|
||||
nfcPutData(0xD0, timestampBytes);
|
||||
nfcPutData(0xC9, key.getFingerprint());
|
||||
} else {
|
||||
throw new IOException("Inappropriate key flags for smart card key.");
|
||||
}
|
||||
|
||||
inputParcel.addCryptoData(subkeyBytes, cardSerialNumber);
|
||||
}
|
||||
|
||||
byte[] subKeyId = new byte[8];
|
||||
ByteBuffer buf = ByteBuffer.wrap(subKeyId);
|
||||
buf.putLong(mRequiredInput.getSubKeyId());
|
||||
inputParcel.addCryptoData(subKeyId, cardSerialNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,13 +179,23 @@ public class SubkeysAdapter extends CursorAdapter {
|
||||
? mSaveKeyringParcel.getSubkeyChange(keyId)
|
||||
: null;
|
||||
|
||||
if (change != null && change.mDummyStrip) {
|
||||
algorithmStr.append(", ");
|
||||
final SpannableString boldStripped = new SpannableString(
|
||||
context.getString(R.string.key_stripped)
|
||||
);
|
||||
boldStripped.setSpan(new StyleSpan(Typeface.BOLD), 0, boldStripped.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
algorithmStr.append(boldStripped);
|
||||
if (change != null && (change.mDummyStrip || change.mDummyDivert != null)) {
|
||||
if (change.mDummyStrip) {
|
||||
algorithmStr.append(", ");
|
||||
final SpannableString boldStripped = new SpannableString(
|
||||
context.getString(R.string.key_stripped)
|
||||
);
|
||||
boldStripped.setSpan(new StyleSpan(Typeface.BOLD), 0, boldStripped.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
algorithmStr.append(boldStripped);
|
||||
}
|
||||
if (change.mDummyDivert != null) {
|
||||
algorithmStr.append(", ");
|
||||
final SpannableString boldDivert = new SpannableString(
|
||||
context.getString(R.string.key_divert)
|
||||
);
|
||||
boldDivert.setSpan(new StyleSpan(Typeface.BOLD), 0, boldDivert.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
algorithmStr.append(boldDivert);
|
||||
}
|
||||
} else {
|
||||
switch (SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET))) {
|
||||
case GNU_DUMMY:
|
||||
|
@ -60,11 +60,9 @@ import org.sufficientlysecure.keychain.util.Preferences;
|
||||
public abstract class BaseNfcActivity extends BaseActivity {
|
||||
|
||||
public static final int REQUEST_CODE_PIN = 1;
|
||||
public static final int REQUEST_CODE_PASSPHRASE = 2;
|
||||
|
||||
protected Passphrase mPin;
|
||||
protected Passphrase mAdminPin;
|
||||
protected Passphrase mPassphrase; // For key export
|
||||
protected boolean mPw1ValidForMultipleSignatures;
|
||||
protected boolean mPw1ValidatedForSignature;
|
||||
protected boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
|
||||
@ -136,15 +134,6 @@ public abstract class BaseNfcActivity extends BaseActivity {
|
||||
enableNfcForegroundDispatch();
|
||||
}
|
||||
|
||||
protected void obtainKeyExportPassphrase(RequiredInputParcel requiredInput) {
|
||||
|
||||
Intent intent = new Intent(this, PassphraseDialogActivity.class);
|
||||
intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
|
||||
RequiredInputParcel.createRequiredPassphrase(requiredInput));
|
||||
startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
|
||||
|
||||
}
|
||||
|
||||
protected void obtainYubiKeyPin(RequiredInputParcel requiredInput) {
|
||||
|
||||
Preferences prefs = Preferences.getPreferences(this);
|
||||
@ -178,17 +167,6 @@ public abstract class BaseNfcActivity extends BaseActivity {
|
||||
break;
|
||||
}
|
||||
|
||||
case REQUEST_CODE_PASSPHRASE: {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
setResult(resultCode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
|
||||
mPassphrase = input.getPassphrase();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
@ -627,7 +605,7 @@ public abstract class BaseNfcActivity extends BaseActivity {
|
||||
* 0xB8: Decipherment Key
|
||||
* 0xA4: Authentication Key
|
||||
*/
|
||||
public void nfcPutKey(int slot, CanonicalizedSecretKey secretKey)
|
||||
public void nfcPutKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
|
||||
throws IOException {
|
||||
if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
|
||||
throw new IOException("Invalid key slot");
|
||||
@ -635,7 +613,7 @@ public abstract class BaseNfcActivity extends BaseActivity {
|
||||
|
||||
RSAPrivateCrtKey crtSecretKey = null;
|
||||
try {
|
||||
secretKey.unlock(mPassphrase);
|
||||
secretKey.unlock(passphrase);
|
||||
crtSecretKey = secretKey.getCrtSecretKey();
|
||||
} catch (PgpGeneralException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
|
@ -928,6 +928,7 @@
|
||||
<string name="msg_mf_error_pgp">"Internal OpenPGP error!"</string>
|
||||
<string name="msg_mf_error_sig">"Signature exception!"</string>
|
||||
<string name="msg_mf_error_subkey_missing">"Tried to operate on missing subkey %s!"</string>
|
||||
<string name="msg_mf_error_conflicting_nfc_commands">"Cannot move key to card in same operation that creates an on-card signature."</string>
|
||||
<string name="msg_mf_master">"Modifying master certifications"</string>
|
||||
<string name="msg_mf_notation_empty">"Adding empty notation packet"</string>
|
||||
<string name="msg_mf_notation_pin">"Adding PIN notation packet"</string>
|
||||
@ -946,6 +947,8 @@
|
||||
<string name="msg_mf_error_past_expiry">"Expiry date cannot be in the past!"</string>
|
||||
<string name="msg_mf_subkey_revoke">"Revoking subkey %s"</string>
|
||||
<string name="msg_mf_subkey_strip">"Stripping subkey %s"</string>
|
||||
<string name="msg_mf_keytocard_start">"Moving subkey %s to smart card"</string>
|
||||
<string name="msg_mf_keytocard_finish">"Moved %1$s to card %2$s"</string>
|
||||
<string name="msg_mf_success">"Keyring successfully modified"</string>
|
||||
<string name="msg_mf_uid_add">"Adding user ID %s"</string>
|
||||
<string name="msg_mf_uid_primary">"Changing primary user ID to %s"</string>
|
||||
|
Loading…
Reference in New Issue
Block a user