open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyToken...

208 lines
8.8 KiB
Java

/*
* Copyright (C) 2018 Schürmann & Breitmoser GbR
*
* 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.securitytoken.operations;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.util.Arrays;
import androidx.annotation.VisibleForTesting;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.securitytoken.CardException;
import org.sufficientlysecure.keychain.securitytoken.CommandApdu;
import org.sufficientlysecure.keychain.securitytoken.EcKeyFormat;
import org.sufficientlysecure.keychain.securitytoken.KeyFormat;
import org.sufficientlysecure.keychain.securitytoken.KeyType;
import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities;
import org.sufficientlysecure.keychain.securitytoken.RsaKeyFormat;
import org.sufficientlysecure.keychain.securitytoken.ResponseApdu;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenUtils;
import org.sufficientlysecure.keychain.util.Passphrase;
public class SecurityTokenChangeKeyTokenOp {
private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
private final SecurityTokenConnection connection;
public static SecurityTokenChangeKeyTokenOp create(SecurityTokenConnection stConnection) {
return new SecurityTokenChangeKeyTokenOp(stConnection);
}
private SecurityTokenChangeKeyTokenOp(SecurityTokenConnection connection) {
this.connection = connection;
}
public void changeKey(CanonicalizedSecretKey secretKey, Passphrase passphrase, Passphrase adminPin) throws IOException {
long keyGenerationTimestamp = secretKey.getCreationTime().getTime() / 1000;
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
KeyType keyType = KeyType.from(secretKey);
if (keyType == null) {
throw new IOException("Inappropriate key flags for smart card key.");
}
// Slot is empty, or contains this key already. PUT KEY operation is safe
boolean canPutKey = isSlotEmpty(keyType)
|| keyMatchesFingerPrint(keyType, secretKey.getFingerprint());
if (!canPutKey) {
throw new IOException(String.format("Key slot occupied; card must be reset to put new %s key.",
keyType.toString()));
}
putKey(keyType, secretKey, passphrase, adminPin);
putData(adminPin, keyType.getFingerprintObjectId(), secretKey.getFingerprint());
putData(adminPin, keyType.getTimestampObjectId(), timestampBytes);
}
/**
* Puts a key on the token in the given slot.
*
* @param slot The slot on the token where the key should be stored:
* 0xB6: Signature Key
* 0xB8: Decipherment Key
* 0xA4: Authentication Key
*/
@VisibleForTesting
void putKey(KeyType slot, CanonicalizedSecretKey secretKey, Passphrase passphrase, Passphrase adminPin)
throws IOException {
RSAPrivateCrtKey crtSecretKey;
ECPrivateKey ecSecretKey;
ECPublicKey ecPublicKey;
connection.verifyAdminPin(adminPin);
// Now we're ready to communicate with the token.
byte[] keyBytes;
try {
secretKey.unlock(passphrase);
byte[] attributesForSecretKey = createAttributesForSecretKey(slot, secretKey);
setKeyAttributes(adminPin, slot, attributesForSecretKey);
OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities();
KeyFormat formatForKeyType = openPgpCapabilities.getFormatForKeyType(slot);
if (formatForKeyType instanceof RsaKeyFormat) {
if (!secretKey.isRSA()) {
throw new IOException("Security Token not configured for RSA key.");
}
crtSecretKey = secretKey.getSecurityTokenRSASecretKey();
// Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537.
if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) {
throw new IOException("Invalid public exponent for smart Security Token.");
}
keyBytes = SecurityTokenUtils.createRSAPrivKeyTemplate(crtSecretKey, slot,
(RsaKeyFormat) formatForKeyType);
} else if (formatForKeyType instanceof EcKeyFormat) {
if (!secretKey.isEC()) {
throw new IOException("Security Token not configured for EC key.");
}
secretKey.unlock(passphrase);
ecSecretKey = secretKey.getSecurityTokenECSecretKey();
ecPublicKey = secretKey.getSecurityTokenECPublicKey();
keyBytes = SecurityTokenUtils.createECPrivKeyTemplate(ecSecretKey, ecPublicKey, slot,
(EcKeyFormat) formatForKeyType);
} else {
throw new IOException("Key type unsupported by security token.");
}
} catch (PgpGeneralException e) {
throw new IOException(e.getMessage());
}
CommandApdu apdu = connection.getCommandFactory().createPutKeyCommand(keyBytes);
ResponseApdu response = connection.communicate(apdu);
if (!response.isSuccess()) {
throw new CardException("Key export to Security Token failed", response.getSw());
}
}
private byte[] createAttributesForSecretKey(KeyType slot, CanonicalizedSecretKey secretKey) throws IOException {
OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities();
KeyFormat formatForKeyType = openPgpCapabilities.getFormatForKeyType(slot);
return SecurityTokenUtils.attributesFromSecretKey(slot, secretKey, formatForKeyType);
}
private void setKeyAttributes(Passphrase adminPin, KeyType keyType, byte[] data) throws IOException {
if (!connection.getOpenPgpCapabilities().isAttributesChangable()) {
return;
}
putData(adminPin, keyType.getAlgoAttributeSlot(), data);
connection.refreshConnectionCapabilities();
}
/**
* Stores a data object on the token. Automatically validates the proper PIN for the operation.
* Supported for all data objects < 255 bytes in length. Only the cardholder certificate
* (0x7F21) can exceed this length.
*
* @param dataObject The data object to be stored.
* @param data The data to store in the object
*/
private void putData(Passphrase adminPin, int dataObject, byte[] data) throws IOException {
if (data.length > 254) {
throw new IOException("Cannot PUT DATA with length > 254");
}
// TODO use admin pin regardless, if we have it?
if (dataObject == 0x0101 || dataObject == 0x0103) {
connection.verifyPinForOther();
} else {
connection.verifyAdminPin(adminPin);
}
CommandApdu command = connection.getCommandFactory().createPutDataCommand(dataObject, data);
ResponseApdu response = connection.communicate(command);
if (!response.isSuccess()) {
throw new CardException("Failed to put data.", response.getSw());
}
}
private boolean isSlotEmpty(KeyType keyType) throws IOException {
// Note: special case: This should not happen, but happens with
// https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
if (connection.getOpenPgpCapabilities().getKeyFingerprint(keyType) == null) {
return true;
}
return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT);
}
private boolean keyMatchesFingerPrint(KeyType keyType, byte[] expectedFingerprint) throws IOException {
byte[] actualFp = connection.getOpenPgpCapabilities().getKeyFingerprint(keyType);
return Arrays.equals(actualFp, expectedFingerprint);
}
}