blob: cd80190c1f642dc7e3438aed7fbd749cff40b8ac [file] [log] [blame]
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.google.crypto.tink;
import com.google.crypto.tink.annotations.Alpha;
import com.google.crypto.tink.internal.LegacyProtoKey;
import com.google.crypto.tink.internal.MutableSerializationRegistry;
import com.google.crypto.tink.internal.ProtoKeySerialization;
import com.google.crypto.tink.monitoring.MonitoringAnnotations;
import com.google.crypto.tink.proto.EncryptedKeyset;
import com.google.crypto.tink.proto.KeyData;
import com.google.crypto.tink.proto.KeyStatusType;
import com.google.crypto.tink.proto.Keyset;
import com.google.crypto.tink.proto.KeysetInfo;
import com.google.crypto.tink.proto.OutputPrefixType;
import com.google.crypto.tink.tinkkey.KeyAccess;
import com.google.crypto.tink.tinkkey.KeyHandle;
import com.google.crypto.tink.tinkkey.internal.InternalKeyHandle;
import com.google.crypto.tink.tinkkey.internal.ProtoKey;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.ByteString;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
/**
* A KeysetHandle provides abstracted access to {@link Keyset}, to limit the exposure of actual
* protocol buffers that hold sensitive key material.
*
* <p>This class allows reading and writing encrypted keysets. Users that want to read or write can
* use the restricted API {@link CleartextKeysetHandle}. Users can also load keysets that don't
* contain any secret key material with {@link NoSecretKeysetHandle}.
*
* @since 1.0.0
*/
public final class KeysetHandle {
/**
* Represents a single entry in a keyset.
*
* <p>An entry in a keyset consists of a key, its ID, and the {@link KeyStatus}. In addition,
* there is one key marked as a primary.
*
* <p>The ID should be considered unique (though currently Tink still accepts keysets with
* repeated IDs). The {@code KeyStatus} tells Tink whether the key should still be used or not.
* There should always be exactly one key which is marked as a primary, however, at the moment
* Tink still accepts keysets which have none. This will be changed in the future.
*/
@Alpha
@Immutable
public static final class Entry {
private Entry(Key key, KeyStatus keyStatus, int id, boolean isPrimary) {
this.key = key;
this.keyStatus = keyStatus;
this.id = id;
this.isPrimary = isPrimary;
}
private final Key key;
private final KeyStatus keyStatus;
private final int id;
private final boolean isPrimary;
/**
* May return an internal class {@link com.google.crypto.tink.internal.LegacyProtoKey} in case
* there is no implementation of the corresponding key class yet.
*/
public Key getKey() {
return key;
}
public KeyStatus getStatus() {
return keyStatus;
}
public int getId() {
return id;
}
/**
* Guaranteed to be true in exactly one entry.
*
* <p>Note: currently this may be false for all entries, since it is possible that keysets are
* parsed without a primary. In the future, such keysets will be rejected when the keyset is
* parsed.
*/
public boolean isPrimary() {
return isPrimary;
}
}
private static KeyStatus parseStatus(KeyStatusType in) throws GeneralSecurityException {
switch (in) {
case ENABLED:
return KeyStatus.ENABLED;
case DISABLED:
return KeyStatus.DISABLED;
case DESTROYED:
return KeyStatus.DESTROYED;
default:
throw new GeneralSecurityException("Unknown key status");
}
}
private static ProtoKeySerialization toProtoKeySerialization(Keyset.Key protoKey) {
int id = protoKey.getKeyId();
@Nullable
Integer idRequirement = protoKey.getOutputPrefixType() == OutputPrefixType.RAW ? null : id;
try {
return ProtoKeySerialization.create(
protoKey.getKeyData().getTypeUrl(),
protoKey.getKeyData().getValue(),
protoKey.getKeyData().getKeyMaterialType(),
protoKey.getOutputPrefixType(),
idRequirement);
} catch (GeneralSecurityException e) {
// Cannot happen -- this only happens if the idRequirement doesn't match OutputPrefixType
throw new IllegalStateException("Creating a protokey serialization failed", e);
}
}
/**
* Returns a Key object from a protoKeySerialization, even if no parser has been registerd.
*
* <p>Falling back is useful because we want users to be able to call {@code #getAt} even for key
* types for which we did not yet register a parser; in this case we simply fall back to return a
* LegacyProtoKey.
*/
private static Key parseWithLegacyFallback(ProtoKeySerialization protoKeySerialization) {
try {
return MutableSerializationRegistry.globalInstance()
.parseKey(protoKeySerialization, InsecureSecretKeyAccess.get());
} catch (GeneralSecurityException e) {
try {
return new LegacyProtoKey(protoKeySerialization, InsecureSecretKeyAccess.get());
} catch (GeneralSecurityException e2) {
// Cannot happen -- this only throws if we have no access.
throw new IllegalStateException("Creating a LegacyProtoKey failed", e2);
}
}
}
private KeysetHandle.Entry entryByIndex(int i) {
Keyset.Key protoKey = keyset.getKey(i);
int id = protoKey.getKeyId();
ProtoKeySerialization protoKeySerialization = toProtoKeySerialization(protoKey);
Key key = parseWithLegacyFallback(protoKeySerialization);
try {
return new KeysetHandle.Entry(
key, parseStatus(protoKey.getStatus()), id, id == keyset.getPrimaryKeyId());
} catch (GeneralSecurityException e) {
// Cannot happen -- this only happens if protoKey.getStatus() fails.
throw new IllegalStateException("Creating an entry failed", e);
}
}
private final Keyset keyset;
private final MonitoringAnnotations annotations;
private KeysetHandle(Keyset keyset) {
this.keyset = keyset;
this.annotations = MonitoringAnnotations.EMPTY;
}
private KeysetHandle(Keyset keyset, MonitoringAnnotations annotations) {
this.keyset = keyset;
this.annotations = annotations;
}
/**
* @return a new {@link KeysetHandle} from a {@code keyset}.
* @throws GeneralSecurityException if the keyset is null or empty.
*/
static final KeysetHandle fromKeyset(Keyset keyset) throws GeneralSecurityException {
assertEnoughKeyMaterial(keyset);
return new KeysetHandle(keyset);
}
/**
* @return a new {@link KeysetHandle} from a {@code keyset} and {@code annotations}.
* @throws GeneralSecurityException if the keyset is null or empty.
*/
static final KeysetHandle fromKeysetAndAnnotations(
Keyset keyset, MonitoringAnnotations annotations) throws GeneralSecurityException {
assertEnoughKeyMaterial(keyset);
return new KeysetHandle(keyset, annotations);
}
/**
* @return the actual keyset data.
*/
Keyset getKeyset() {
return keyset;
}
/**
* Returns the unique entry where isPrimary() = true and getStatus() = ENABLED.
*
* <p>Note: currently this may throw IllegalStateException, since it is possible that keysets are
* parsed without a primary. In the future, such keysets will be rejected when the keyset is
* parsed.
*/
public KeysetHandle.Entry getPrimary() {
for (int i = 0; i < keyset.getKeyCount(); ++i) {
if (keyset.getKey(i).getKeyId() == keyset.getPrimaryKeyId()) {
Entry result = entryByIndex(i);
if (result.getStatus() != KeyStatus.ENABLED) {
throw new IllegalStateException("Keyset has primary which isn't enabled");
}
return result;
}
}
throw new IllegalStateException("Keyset has no primary");
}
/** Returns the size of this keyset. */
public int size() {
return keyset.getKeyCount();
}
/**
* Returns the entry at index i. The order is preserved and depends on the order at which the
* entries were inserted when the KeysetHandle was built.
*
* <p>Currently, this may throw "IllegalStateException" in case the status entry of the Key in the
* keyset was wrongly set. In the future, Tink will throw at parsing time in this case.
*
* @throws IndexOutOfBoundsException if i < 0 or i >= size();
*/
public KeysetHandle.Entry getAt(int i) {
if (i < 0 || i >= size()) {
throw new IndexOutOfBoundsException("Invalid index " + i + " for keyset of size " + size());
}
return entryByIndex(i);
}
/** Returns the keyset data as a list of {@link KeyHandle}s. */
public List<KeyHandle> getKeys() {
ArrayList<KeyHandle> result = new ArrayList<>();
for (Keyset.Key key : keyset.getKeyList()) {
KeyData keyData = key.getKeyData();
result.add(
new InternalKeyHandle(
new ProtoKey(keyData, KeyTemplate.fromProto(key.getOutputPrefixType())),
key.getStatus(),
key.getKeyId()));
}
return Collections.unmodifiableList(result);
}
/**
* @return the {@link com.google.crypto.tink.proto.KeysetInfo} that doesn't contain actual key
* material.
*/
public KeysetInfo getKeysetInfo() {
return Util.getKeysetInfo(keyset);
}
/**
* Generates a new {@link KeysetHandle} that contains a single fresh key generated according to
* {@code keyTemplate}.
*
* @throws GeneralSecurityException if the key template is invalid.
* @deprecated This method takes a KeyTemplate proto, which is an internal implementation detail.
* Please use the generateNew method that takes a {@link KeyTemplate} POJO.
*/
@Deprecated
public static final KeysetHandle generateNew(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
throws GeneralSecurityException {
return KeysetManager.withEmptyKeyset().rotate(keyTemplate).getKeysetHandle();
}
/**
* Generates a new {@link KeysetHandle} that contains a single fresh key generated according to
* {@code keyTemplate}.
*
* @throws GeneralSecurityException if the key template is invalid.
*/
public static final KeysetHandle generateNew(KeyTemplate keyTemplate)
throws GeneralSecurityException {
return KeysetManager.withEmptyKeyset().rotate(keyTemplate.getProto()).getKeysetHandle();
}
/**
* Returns a {@code KeysetHandle} that contains the single {@code KeyHandle} passed as input.
*
* @deprecated Use {@code KeysetManager.withEmptyKeyset().add(keyHandle)
* .setPrimary(keyHandle.getId()).getKeysetHandle()} instead.
*/
@Deprecated
public static final KeysetHandle createFromKey(KeyHandle keyHandle, KeyAccess access)
throws GeneralSecurityException {
KeysetManager km = KeysetManager.withEmptyKeyset().add(keyHandle);
km.setPrimary(km.getKeysetHandle().getKeysetInfo().getKeyInfo(0).getKeyId());
return km.getKeysetHandle();
}
/**
* Tries to create a {@link KeysetHandle} from an encrypted keyset obtained via {@code reader}.
*
* <p>Users that need to load cleartext keysets can use {@link CleartextKeysetHandle}.
*
* @return a new {@link KeysetHandle} from {@code encryptedKeysetProto} that was encrypted with
* {@code masterKey}
* @throws GeneralSecurityException if cannot decrypt the keyset or it doesn't contain encrypted
* key material
*/
public static final KeysetHandle read(KeysetReader reader, Aead masterKey)
throws GeneralSecurityException, IOException {
return readWithAssociatedData(reader, masterKey, new byte[0]);
}
/**
* Tries to create a {@link KeysetHandle} from an encrypted keyset obtained via {@code reader},
* using the provided associated data.
*
* <p>Users that need to load cleartext keysets can use {@link CleartextKeysetHandle}.
*
* @return a new {@link KeysetHandle} from {@code encryptedKeysetProto} that was encrypted with
* {@code masterKey}
* @throws GeneralSecurityException if cannot decrypt the keyset or it doesn't contain encrypted
* key material
*/
public static final KeysetHandle readWithAssociatedData(
KeysetReader reader, Aead masterKey, byte[] associatedData)
throws GeneralSecurityException, IOException {
EncryptedKeyset encryptedKeyset = reader.readEncrypted();
assertEnoughEncryptedKeyMaterial(encryptedKeyset);
return new KeysetHandle(decrypt(encryptedKeyset, masterKey, associatedData));
}
/**
* Tries to create a {@link KeysetHandle} from a keyset, obtained via {@code reader}, which
* contains no secret key material.
*
* <p>This can be used to load public keysets or envelope encryption keysets. Users that need to
* load cleartext keysets can use {@link CleartextKeysetHandle}.
*
* @return a new {@link KeysetHandle} from {@code serialized} that is a serialized {@link Keyset}
* @throws GeneralSecurityException if the keyset is invalid
*/
@SuppressWarnings("UnusedException")
public static final KeysetHandle readNoSecret(KeysetReader reader)
throws GeneralSecurityException, IOException {
try {
Keyset keyset = reader.read();
assertNoSecretKeyMaterial(keyset);
return KeysetHandle.fromKeyset(keyset);
} catch (InvalidProtocolBufferException e) {
// Do not propagate InvalidProtocolBufferException to guarantee no key material is leaked
throw new GeneralSecurityException("invalid keyset");
}
}
/**
* Tries to create a {@link KeysetHandle} from a serialized keyset which contains no secret key
* material.
*
* <p>This can be used to load public keysets or envelope encryption keysets. Users that need to
* load cleartext keysets can use {@link CleartextKeysetHandle}.
*
* @return a new {@link KeysetHandle} from {@code serialized} that is a serialized {@link Keyset}
* @throws GeneralSecurityException if the keyset is invalid
*/
@SuppressWarnings("UnusedException")
public static final KeysetHandle readNoSecret(final byte[] serialized)
throws GeneralSecurityException {
try {
Keyset keyset = Keyset.parseFrom(serialized, ExtensionRegistryLite.getEmptyRegistry());
assertNoSecretKeyMaterial(keyset);
return KeysetHandle.fromKeyset(keyset);
} catch (InvalidProtocolBufferException e) {
// Do not propagate InvalidProtocolBufferException to guarantee no key material is leaked
throw new GeneralSecurityException("invalid keyset");
}
}
/** Serializes, encrypts with {@code masterKey} and writes the keyset to {@code outputStream}. */
public void write(KeysetWriter keysetWriter, Aead masterKey)
throws GeneralSecurityException, IOException {
writeWithAssociatedData(keysetWriter, masterKey, new byte[0]);
}
/**
* Serializes, encrypts with {@code masterKey} and writes the keyset to {@code outputStream} using
* the provided associated data.
*/
public void writeWithAssociatedData(
KeysetWriter keysetWriter, Aead masterKey, byte[] associatedData)
throws GeneralSecurityException, IOException {
EncryptedKeyset encryptedKeyset = encrypt(keyset, masterKey, associatedData);
keysetWriter.write(encryptedKeyset);
return;
}
/**
* Tries to write to {@code writer} this keyset which must not contain any secret key material.
*
* <p>This can be used to persist public keysets or envelope encryption keysets. Users that need
* to persist cleartext keysets can use {@link CleartextKeysetHandle}.
*
* @throws GeneralSecurityException if the keyset contains any secret key material
*/
public void writeNoSecret(KeysetWriter writer) throws GeneralSecurityException, IOException {
assertNoSecretKeyMaterial(keyset);
writer.write(keyset);
return;
}
/** Encrypts the keyset with the {@link Aead} master key. */
@SuppressWarnings("UnusedException")
private static EncryptedKeyset encrypt(Keyset keyset, Aead masterKey, byte[] associatedData)
throws GeneralSecurityException {
byte[] encryptedKeyset = masterKey.encrypt(keyset.toByteArray(), associatedData);
// Check if we can decrypt, to detect errors
try {
final Keyset keyset2 =
Keyset.parseFrom(
masterKey.decrypt(encryptedKeyset, associatedData),
ExtensionRegistryLite.getEmptyRegistry());
if (!keyset2.equals(keyset)) {
throw new GeneralSecurityException("cannot encrypt keyset");
}
} catch (InvalidProtocolBufferException e) {
// Do not propagate InvalidProtocolBufferException to guarantee no key material is leaked
throw new GeneralSecurityException("invalid keyset, corrupted key material");
}
return EncryptedKeyset.newBuilder()
.setEncryptedKeyset(ByteString.copyFrom(encryptedKeyset))
.setKeysetInfo(Util.getKeysetInfo(keyset))
.build();
}
/** Decrypts the encrypted keyset with the {@link Aead} master key. */
@SuppressWarnings("UnusedException")
private static Keyset decrypt(
EncryptedKeyset encryptedKeyset, Aead masterKey, byte[] associatedData)
throws GeneralSecurityException {
try {
Keyset keyset =
Keyset.parseFrom(
masterKey.decrypt(encryptedKeyset.getEncryptedKeyset().toByteArray(), associatedData),
ExtensionRegistryLite.getEmptyRegistry());
// check emptiness here too, in case the encrypted keys unwrapped to nothing?
assertEnoughKeyMaterial(keyset);
return keyset;
} catch (
InvalidProtocolBufferException e) {
// Do not propagate InvalidProtocolBufferException to guarantee no key material is leaked
throw new GeneralSecurityException("invalid keyset, corrupted key material");
}
}
/**
* If the managed keyset contains private keys, returns a {@link KeysetHandle} of the public keys.
*
* @throws GenernalSecurityException if the managed keyset is null or if it contains any
* non-private keys.
*/
public KeysetHandle getPublicKeysetHandle() throws GeneralSecurityException {
if (keyset == null) {
throw new GeneralSecurityException("cleartext keyset is not available");
}
Keyset.Builder keysetBuilder = Keyset.newBuilder();
for (Keyset.Key key : keyset.getKeyList()) {
KeyData keyData = createPublicKeyData(key.getKeyData());
keysetBuilder.addKey(key.toBuilder().setKeyData(keyData).build());
}
keysetBuilder.setPrimaryKeyId(keyset.getPrimaryKeyId());
return new KeysetHandle(keysetBuilder.build());
}
private static KeyData createPublicKeyData(KeyData privateKeyData)
throws GeneralSecurityException {
if (privateKeyData.getKeyMaterialType() != KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE) {
throw new GeneralSecurityException("The keyset contains a non-private key");
}
KeyData publicKeyData =
Registry.getPublicKeyData(privateKeyData.getTypeUrl(), privateKeyData.getValue());
validate(publicKeyData);
return publicKeyData;
}
@SuppressWarnings("deprecation")
private static void validate(KeyData keyData) throws GeneralSecurityException {
// This will throw GeneralSecurityException if the keyData is invalid.
Registry.getPrimitive(keyData);
}
/**
* Extracts and returns the string representation of the {@link
* com.google.crypto.tink.proto.KeysetInfo} of the managed keyset.
*/
@SuppressWarnings("LiteProtoToString") // main purpose of toString is for debugging
@Override
public String toString() {
return getKeysetInfo().toString();
}
/**
* Validates that {@code keyset} doesn't contain any secret key material.
*
* @throws GeneralSecurityException if {@code keyset} contains secret key material.
*/
private static void assertNoSecretKeyMaterial(Keyset keyset) throws GeneralSecurityException {
for (Keyset.Key key : keyset.getKeyList()) {
if (key.getKeyData().getKeyMaterialType() == KeyData.KeyMaterialType.UNKNOWN_KEYMATERIAL
|| key.getKeyData().getKeyMaterialType() == KeyData.KeyMaterialType.SYMMETRIC
|| key.getKeyData().getKeyMaterialType() == KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE) {
throw new GeneralSecurityException(
String.format(
"keyset contains key material of type %s for type url %s",
key.getKeyData().getKeyMaterialType().name(), key.getKeyData().getTypeUrl()));
}
}
}
/**
* Validates that a keyset handle contains enough key material to build a keyset on.
*
* @throws GeneralSecurityException if the validation fails
*/
private static void assertEnoughKeyMaterial(Keyset keyset) throws GeneralSecurityException {
if (keyset == null || keyset.getKeyCount() <= 0) {
throw new GeneralSecurityException("empty keyset");
}
}
/**
* Validates that an encrypted keyset contains enough key material to build a keyset on.
*
* @throws GeneralSecurityException if the validation fails
*/
private static void assertEnoughEncryptedKeyMaterial(EncryptedKeyset keyset)
throws GeneralSecurityException {
if (keyset == null || keyset.getEncryptedKeyset().size() == 0) {
throw new GeneralSecurityException("empty keyset");
}
}
/** Helper function to allow us to have a a name {@code B} for the base primitive. */
private <B, P> P getPrimitiveWithKnownInputPrimitive(
Class<P> classObject, Class<B> inputPrimitiveClassObject) throws GeneralSecurityException {
Util.validateKeyset(keyset);
PrimitiveSet.Builder<B> builder = PrimitiveSet.newBuilder(inputPrimitiveClassObject);
builder.setAnnotations(annotations);
for (Keyset.Key key : keyset.getKeyList()) {
if (key.getStatus() == KeyStatusType.ENABLED) {
B primitive = Registry.getPrimitive(key.getKeyData(), inputPrimitiveClassObject);
if (key.getKeyId() == keyset.getPrimaryKeyId()) {
builder.addPrimaryPrimitive(primitive, key);
} else {
builder.addPrimitive(primitive, key);
}
}
}
return Registry.wrap(builder.build(), classObject);
}
/**
* Returns a primitive from this keyset, using the global registry to create resources creating
* the primitive.
*/
public <P> P getPrimitive(Class<P> targetClassObject) throws GeneralSecurityException {
Class<?> inputPrimitiveClassObject = Registry.getInputPrimitive(targetClassObject);
if (inputPrimitiveClassObject == null) {
throw new GeneralSecurityException("No wrapper found for " + targetClassObject.getName());
}
return getPrimitiveWithKnownInputPrimitive(targetClassObject, inputPrimitiveClassObject);
}
/**
* Searches the keyset to find the primary key of this {@code KeysetHandle}, and returns the key
* wrapped in a {@code KeyHandle}.
*/
public KeyHandle primaryKey() throws GeneralSecurityException {
int primaryKeyId = keyset.getPrimaryKeyId();
for (Keyset.Key key : keyset.getKeyList()) {
if (key.getKeyId() == primaryKeyId) {
return new InternalKeyHandle(
new ProtoKey(key.getKeyData(), KeyTemplate.fromProto(key.getOutputPrefixType())),
key.getStatus(),
key.getKeyId());
}
}
throw new GeneralSecurityException("No primary key found in keyset.");
}
}