All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.exceptionfactory.jagged.ssh.SshEd25519RecipientStanzaReader Maven / Gradle / Ivy

/*
 * Copyright 2023 Jagged Contributors
 *
 * 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.exceptionfactory.jagged.ssh;

import com.exceptionfactory.jagged.FileKey;
import com.exceptionfactory.jagged.RecipientStanza;
import com.exceptionfactory.jagged.RecipientStanzaReader;
import com.exceptionfactory.jagged.UnsupportedRecipientStanzaException;
import com.exceptionfactory.jagged.framework.codec.CanonicalBase64;
import com.exceptionfactory.jagged.framework.crypto.CipherKey;
import com.exceptionfactory.jagged.framework.crypto.EncryptedFileKey;
import com.exceptionfactory.jagged.framework.crypto.FileKeyDecryptor;
import com.exceptionfactory.jagged.framework.crypto.SharedSecretKey;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import static com.exceptionfactory.jagged.ssh.SshEd25519RecipientIndicator.STANZA_TYPE;

/**
 * SSH Ed25519 implementation of Recipient Stanza Reader compatible with age-ssh
 */
final class SshEd25519RecipientStanzaReader implements RecipientStanzaReader {
    private static final int EPHEMERAL_SHARE_ENCODED_LENGTH = 43;

    private static final int ENCRYPTED_FILE_KEY_LENGTH = 32;

    private static final CanonicalBase64.Decoder BASE64_DECODER = CanonicalBase64.getDecoder();

    private static final SshEd25519PublicKeyMarshaller PUBLIC_KEY_MARSHALLER = new SshEd25519PublicKeyMarshaller();

    private static final PublicKeyFingerprintProducer PUBLIC_KEY_FINGERPRINT_PRODUCER = new StandardPublicKeyFingerprintProducer();

    private final Ed25519KeyConverter keyConverter;

    private final SharedSecretKeyProducer sharedSecretKeyProducer;

    private final SharedSecretKeyProducer derivedSharedSecretKeyProducer;

    private final SharedWrapKeyProducer sharedWrapKeyProducer;

    private final FileKeyDecryptor fileKeyDecryptor;

    private final String publicKeyFingerprint;

    /**
     * SSH Ed25519 Recipient Stanza Reader with Ed25519 Key Pair for decryption of File Key
     *
     * @param publicKey Ed25519 Public Key
     * @param privateKey Ed25519 Private Key
     * @param keyPairGeneratorFactory X25519 Key Pair Generator Factory
     * @param keyAgreementFactory X25519 Key Agreement Factory
     * @param fileKeyDecryptor File Key Decryptor
     * @throws GeneralSecurityException Thrown on failures to derive Public Key
     */
    SshEd25519RecipientStanzaReader(
            final Ed25519PublicKey publicKey,
            final Ed25519PrivateKey privateKey,
            final X25519KeyPairGeneratorFactory keyPairGeneratorFactory,
            final X25519KeyAgreementFactory keyAgreementFactory,
            final FileKeyDecryptor fileKeyDecryptor
    ) throws GeneralSecurityException {
        Objects.requireNonNull(publicKey, "Public Key required");
        Objects.requireNonNull(privateKey, "Private Key required");
        Objects.requireNonNull(keyPairGeneratorFactory, "Key Pair Generator Factory required");
        Objects.requireNonNull(keyAgreementFactory, "Key Agreement Factory required");
        this.fileKeyDecryptor = Objects.requireNonNull(fileKeyDecryptor, "File Key Decryptor required");

        final byte[] marshalledKey = PUBLIC_KEY_MARSHALLER.getMarshalledKey(publicKey);
        publicKeyFingerprint = PUBLIC_KEY_FINGERPRINT_PRODUCER.getFingerprint(marshalledKey);

        keyConverter = new StandardEd25519KeyConverter(keyPairGeneratorFactory);
        final PrivateKey privateKeyConverted = keyConverter.getPrivateKey(privateKey);
        sharedSecretKeyProducer = new X25519SharedSecretKeyProducer(privateKeyConverted, keyAgreementFactory);
        sharedWrapKeyProducer = getWrapKeyProducer();

        final SshEd25519SharedWrapKeyProducer sshEd25519SharedWrapKeyProducer = new SshEd25519SharedWrapKeyProducer();
        final SshEd25519DerivedKey sshEd25519DerivedKey = sshEd25519SharedWrapKeyProducer.getDerivedKey(publicKey);
        final PrivateKey derivedPrivateKey = keyConverter.getPrivateKey(sshEd25519DerivedKey);
        derivedSharedSecretKeyProducer = new X25519SharedSecretKeyProducer(derivedPrivateKey, keyAgreementFactory);
    }

    /**
     * Get File Key from matching ssh-ed25519 Recipient Stanza
     *
     * @param recipientStanzas One or more Recipient Stanzas parsed from the age file header
     * @return File Key decrypted from matching ssh-ed25519 Recipient Stanza arguments
     * @throws GeneralSecurityException Thrown on failure to read or decrypt File Key
     */
    @Override
    public FileKey getFileKey(final Iterable recipientStanzas) throws GeneralSecurityException {
        Objects.requireNonNull(recipientStanzas, "Recipient Stanzas required");

        final List exceptions = new ArrayList<>();
        for (final RecipientStanza recipientStanza : recipientStanzas) {
            final String recipientStanzaType = recipientStanza.getType();
            if (STANZA_TYPE.getIndicator().equals(recipientStanzaType)) {
                try {
                    return getFileKey(recipientStanza);
                } catch (final Exception e) {
                    exceptions.add(e);
                }
            }
        }

        if (exceptions.isEmpty()) {
            throw new UnsupportedRecipientStanzaException(String.format("%s Recipient Stanzas not found", STANZA_TYPE.getIndicator()));
        } else {
            final String message = String.format("%s Recipient Stanza not matched", STANZA_TYPE.getIndicator());
            final UnsupportedRecipientStanzaException exception = new UnsupportedRecipientStanzaException(message);
            exceptions.forEach(exception::addSuppressed);
            throw exception;
        }
    }

    private FileKey getFileKey(final RecipientStanza recipientStanza) throws GeneralSecurityException {
        final Iterator recipientStanzaArguments = recipientStanza.getArguments().iterator();
        final String recipientKeyFingerprint = getRecipientKeyFingerprint(recipientStanzaArguments);

        if (publicKeyFingerprint.equals(recipientKeyFingerprint)) {
            final SharedSecretKey ephemeralSharedSecretKey = getEphemeralSharedSecretKey(recipientStanzaArguments);
            final PublicKey ephemeralSharedPublicKey = keyConverter.getPublicKey(ephemeralSharedSecretKey);
            final SharedSecretKey sharedSecretKey = sharedSecretKeyProducer.getSharedSecretKey(ephemeralSharedPublicKey);
            final PublicKey sharedPublicKey = keyConverter.getPublicKey(sharedSecretKey);

            final SharedSecretKey derivedSharedSecretKey = derivedSharedSecretKeyProducer.getSharedSecretKey(sharedPublicKey);
            final CipherKey wrapKey = sharedWrapKeyProducer.getWrapKey(derivedSharedSecretKey, ephemeralSharedPublicKey);

            final byte[] encryptedFileKeyEncoded = recipientStanza.getBody();
            final int encryptedFileKeyLength = encryptedFileKeyEncoded.length;
            if (encryptedFileKeyLength == ENCRYPTED_FILE_KEY_LENGTH) {
                final EncryptedFileKey encryptedFileKey = new EncryptedFileKey(encryptedFileKeyEncoded);
                return fileKeyDecryptor.getFileKey(encryptedFileKey, wrapKey);
            } else {
                final String message = String.format("Recipient Stanza Body length [%d] not required length [%d]", encryptedFileKeyLength, ENCRYPTED_FILE_KEY_LENGTH);
                throw new UnsupportedRecipientStanzaException(message);
            }
        } else {
            final String message = String.format("%s Recipient Stanza Key Fingerprint [%s] not matched", STANZA_TYPE.getIndicator(), recipientKeyFingerprint);
            throw new UnsupportedRecipientStanzaException(message);
        }
    }

    private SharedSecretKey getEphemeralSharedSecretKey(final Iterator recipientStanzaArguments) throws UnsupportedRecipientStanzaException {
        if (recipientStanzaArguments.hasNext()) {
            final String ephemeralShareEncoded = recipientStanzaArguments.next();

            if (recipientStanzaArguments.hasNext()) {
                final String message = String.format("%s Recipient Stanza extra argument not expected", STANZA_TYPE.getIndicator());
                throw new UnsupportedRecipientStanzaException(message);
            }

            final int encodedLength = ephemeralShareEncoded.length();
            if (EPHEMERAL_SHARE_ENCODED_LENGTH == encodedLength) {
                final byte[] ephemeralShareEncodedBytes = ephemeralShareEncoded.getBytes(StandardCharsets.US_ASCII);
                final byte[] ephemeralShare = BASE64_DECODER.decode(ephemeralShareEncodedBytes);
                return new SharedSecretKey(ephemeralShare);
            } else {
                final String message = String.format("%s ephemeral share length [%d] not expected", STANZA_TYPE.getIndicator(), encodedLength);
                throw new UnsupportedRecipientStanzaException(message);
            }
        } else {
            final String message = String.format("%s ephemeral share argument not found", STANZA_TYPE.getIndicator());
            throw new UnsupportedRecipientStanzaException(message);
        }
    }

    private String getRecipientKeyFingerprint(final Iterator arguments) throws UnsupportedRecipientStanzaException {
        if (arguments.hasNext()) {
            return arguments.next();
        } else {
            throw new UnsupportedRecipientStanzaException("Key Fingerprint argument not found");
        }
    }

    private SharedWrapKeyProducer getWrapKeyProducer() throws GeneralSecurityException {
        final X25519BasePointPublicKey basePointPublicKey = new X25519BasePointPublicKey();
        final SharedSecretKey basePointPublicKeyEncoded = new SharedSecretKey(basePointPublicKey.getEncoded());
        final PublicKey basePointPublicKeyConverted = keyConverter.getPublicKey(basePointPublicKeyEncoded);
        final SharedSecretKey basePointSharedSecretKey = sharedSecretKeyProducer.getSharedSecretKey(basePointPublicKeyConverted);
        final PublicKey recipientPublicKey = keyConverter.getPublicKey(basePointSharedSecretKey);
        return new X25519SharedWrapKeyProducer(recipientPublicKey);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy