// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.security; import org.bouncycastle.util.Arrays; import java.nio.ByteBuffer; import java.security.KeyPair; import java.security.PrivateKey; import java.security.interfaces.XECPublicKey; import java.util.Optional; /** *

Delegated resealing protocol for getting access to a shared secret key of a token * whose private key we do not possess.

* *

The primary benefit of the interactive resealing protocol is that none of the data * exchanged can reveal anything about the underlying sealed secret itself.

* *

Note that neither resealing requests nor responses are authenticated (this is a property * inherited from the sealed shared key tokens themselves). It is assumed that an attacker * can observe all requests and responses in transit, but cannot modify them.

* *

Protocol details

* *

Decryptor (requester):

*
    *
  1. Create a resealing session instance that maintains an ephemeral X25519 key pair that * is valid only for the lifetime of the session.
  2. *
  3. Create a resealing request for a token T. The session emits a Base62-encoded * binary representation of the tuple <ephemeral public key, T>.
  4. *
  5. Send the request to the private key holder. The session must be kept alive until the * response is received, or the ephemeral private key associated with the public key will * be irrevocably lost.
  6. *
*

Private key holder (re-sealer):

*
    *
  1. Decode Base62-encoded request into tuple <ephemeral public key, T>.
  2. *
  3. Look up the correct private key from the key ID contained in token T.
  4. *
  5. Reseal token T for the requested ephemeral public key using the correct private key.
  6. *
  7. Return resealed token TR to requester.
  8. *
*

Decryptor (requester):

*
    *
  1. Decrypt token TR using ephemeral private key.
  2. *
  3. Use secret key in token to decrypt the payload protected by original token T.
  4. *
* * @author vekterli */ public class SharedKeyResealingSession { private final KeyPair ephemeralKeyPair; SharedKeyResealingSession(KeyPair ephemeralKeyPair) { this.ephemeralKeyPair = ephemeralKeyPair; } public static SharedKeyResealingSession newEphemeralSession() { return new SharedKeyResealingSession(KeyUtils.generateX25519KeyPair()); } @FunctionalInterface public interface PrivateKeyProvider { Optional privateKeyForId(KeyId id); } public record ResealingRequest(XECPublicKey ephemeralPubKey, SealedSharedKey sealedKey) { private static final byte[] HEADER_BYTES = new byte[] {'R','S'}; private static final byte CURRENT_VERSION = 1; public String toSerializedString() { byte[] pubKeyBytes = KeyUtils.toRawX25519PublicKeyBytes(ephemeralPubKey); byte[] tokenBytes = sealedKey.toSerializedBytes(); ByteBuffer encoded = ByteBuffer.allocate(HEADER_BYTES.length + 1 + 1 + pubKeyBytes.length + tokenBytes.length); encoded.put(HEADER_BYTES); encoded.put(CURRENT_VERSION); encoded.put((byte)pubKeyBytes.length); encoded.put(pubKeyBytes); encoded.put(tokenBytes); encoded.flip(); byte[] encBytes = new byte[encoded.remaining()]; encoded.get(encBytes); return Base62.codec().encode(encBytes); } public static ResealingRequest fromSerializedString(String request) { verifyInputStringNotTooLarge(request); byte[] rawBytes = Base62.codec().decode(request); if (rawBytes.length < HEADER_BYTES.length + 2) { throw new IllegalArgumentException("Resealing request too short to contain a header and key length"); } ByteBuffer decoded = ByteBuffer.wrap(rawBytes); byte[] header = new byte[2]; decoded.get(header); if (!Arrays.areEqual(header, HEADER_BYTES)) { throw new IllegalArgumentException("No resealing request header found"); } byte version = decoded.get(); if (version != CURRENT_VERSION) { throw new IllegalArgumentException("Unsupported version in resealing request header"); } int pubKeyLen = Byte.toUnsignedInt(decoded.get()); byte[] pubKeyBytes = new byte[pubKeyLen]; decoded.get(pubKeyBytes); byte[] rawTokenBytes = new byte[decoded.remaining()]; decoded.get(rawTokenBytes); return new ResealingRequest(KeyUtils.fromRawX25519PublicKey(pubKeyBytes), SealedSharedKey.fromSerializedBytes(rawTokenBytes)); } private static void verifyInputStringNotTooLarge(String tokenString) { if (tokenString.length() > SealedSharedKey.MAX_TOKEN_STRING_LENGTH + 64) { throw new IllegalArgumentException("String is too long to possibly be a valid resealing request"); } } } public record ResealingResponse(SealedSharedKey resealedKey) { public String toSerializedString() { return resealedKey.toTokenString(); } public static ResealingResponse fromSerializedString(String response) { return new ResealingResponse(SealedSharedKey.fromTokenString(response)); } } public ResealingRequest resealingRequestFor(SealedSharedKey sealedSharedKey) { return new ResealingRequest((XECPublicKey) ephemeralKeyPair.getPublic(), sealedSharedKey); } public static ResealingResponse reseal(ResealingRequest request, PrivateKeyProvider privateKeyProvider) { var privKey = privateKeyProvider.privateKeyForId(request.sealedKey.keyId()).orElseThrow( () -> new IllegalArgumentException("Could not find a private key for key ID '%s'".formatted(request.sealedKey.keyId()))); var secretShared = SharedKeyGenerator.fromSealedKey(request.sealedKey, privKey); var resealed = SharedKeyGenerator.reseal(secretShared, request.ephemeralPubKey, KeyId.ofString("resealed-token")); // TODO key id return new ResealingResponse(resealed.sealedSharedKey()); } public SecretSharedKey openResealingResponse(ResealingResponse response) { return SharedKeyGenerator.fromSealedKey(response.resealedKey, ephemeralKeyPair.getPrivate()); } }