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

com.exceptionfactory.jagged.ssh.SshEd25519RecipientStanzaWriter 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.RecipientStanzaWriter;
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.FileKeyEncryptor;
import com.exceptionfactory.jagged.framework.crypto.SharedSecretKey;

import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Objects;

/**
 * SSH Ed25519 implementation of Recipient Stanza Writer compatible with age-ssh
 */
final class SshEd25519RecipientStanzaWriter implements RecipientStanzaWriter {
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    private static final CanonicalBase64.Encoder ENCODER = CanonicalBase64.getEncoder();

    private final SshEd25519PublicKeyMarshaller publicKeyMarshaller = new SshEd25519PublicKeyMarshaller();

    private final PublicKeyFingerprintProducer publicKeyFingerprintProducer = new StandardPublicKeyFingerprintProducer();

    private final Ed25519PublicKey publicKey;

    private final PublicKey publicKeyConverted;

    private final Ed25519KeyConverter keyConverter;

    private final X25519KeyAgreementFactory keyAgreementFactory;

    private final FileKeyEncryptor fileKeyEncryptor;

    private final SharedSecretKeyProducer derivedSharedSecretKeyProducer;

    /**
     * SSH Ed25519 Recipient Stanza Writer with Ed25519 Public Key
     *
     * @param publicKey Ed25519 Public Key for recipient of encrypted File Key
     * @param keyPairGeneratorFactory X25519 Key Pair Generator Factory for key processing
     * @param keyAgreementFactory X25519 Key Agreement Factory for key derivation
     * @param fileKeyEncryptor File Key Encryptor
     * @throws GeneralSecurityException Thrown on failures to convert between key formats
     */
    SshEd25519RecipientStanzaWriter(
            final Ed25519PublicKey publicKey,
            final X25519KeyPairGeneratorFactory keyPairGeneratorFactory,
            final X25519KeyAgreementFactory keyAgreementFactory,
            final FileKeyEncryptor fileKeyEncryptor
    ) throws GeneralSecurityException {
        this.publicKey = Objects.requireNonNull(publicKey, "Public Key required");
        this.keyAgreementFactory = Objects.requireNonNull(keyAgreementFactory, "Key Agreement Factory required");
        this.fileKeyEncryptor = Objects.requireNonNull(fileKeyEncryptor, "File Key Encryptor required");
        this.keyConverter = new StandardEd25519KeyConverter(keyPairGeneratorFactory);
        this.publicKeyConverted = keyConverter.getPublicKey(publicKey);

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

    /**
     * Get Recipient Stanzas containing one ssh-ed25519 Recipient Stanza with the encrypted File Key
     *
     * @param fileKey File Key to be encrypted
     * @return Singleton List of ssh-ed25519 Recipient Stanza with encrypted File Key
     * @throws GeneralSecurityException Thrown key derivation or encryption failures
     */
    @Override
    public Iterable getRecipientStanzas(final FileKey fileKey) throws GeneralSecurityException {
        Objects.requireNonNull(fileKey, "File Key required");

        final SharedSecretKeyProducer ephemeralSharedSecretKeyProducer = getEphemeralSharedSecretKeyProducer();
        final PublicKey basePointPublicKey = getBasePointPublicKey();
        final SharedSecretKey ephemeralSharedSecretKey = ephemeralSharedSecretKeyProducer.getSharedSecretKey(basePointPublicKey);

        final CipherKey wrapKey = getWrapKey(ephemeralSharedSecretKeyProducer, ephemeralSharedSecretKey);
        final EncryptedFileKey encryptedFileKey = fileKeyEncryptor.getEncryptedFileKey(fileKey, wrapKey);
        final byte[] encryptedFileKeyEncoded = encryptedFileKey.getEncoded();

        final byte[] marshalledKey = publicKeyMarshaller.getMarshalledKey(publicKey);
        final String keyFingerprint = publicKeyFingerprintProducer.getFingerprint(marshalledKey);
        final String ephemeralShare = ENCODER.encodeToString(ephemeralSharedSecretKey.getEncoded());
        final RecipientStanza recipientStanza = new SshEd25519RecipientStanza(keyFingerprint, ephemeralShare, encryptedFileKeyEncoded);
        return Collections.singletonList(recipientStanza);
    }

    private CipherKey getWrapKey(final SharedSecretKeyProducer ephemeralSharedSecretKeyProducer, final SharedSecretKey ephemeralSharedSecretKey) throws GeneralSecurityException {
        final PublicKey ephemeralSharedPublicKey = keyConverter.getPublicKey(ephemeralSharedSecretKey);
        final SharedSecretKey sharedSecretKey = ephemeralSharedSecretKeyProducer.getSharedSecretKey(publicKeyConverted);
        final PublicKey sharedPublicKey = keyConverter.getPublicKey(sharedSecretKey);

        final SharedSecretKey derivedSharedSecretKey = derivedSharedSecretKeyProducer.getSharedSecretKey(sharedPublicKey);
        final SharedWrapKeyProducer sharedWrapKeyProducer = new X25519SharedWrapKeyProducer(publicKeyConverted);
        return sharedWrapKeyProducer.getWrapKey(derivedSharedSecretKey, ephemeralSharedPublicKey);
    }

    private PublicKey getBasePointPublicKey() throws GeneralSecurityException {
        final X25519BasePointPublicKey basePointPublicKey = new X25519BasePointPublicKey();
        final SharedSecretKey basePointPublicKeyEncoded = new SharedSecretKey(basePointPublicKey.getEncoded());
        return keyConverter.getPublicKey(basePointPublicKeyEncoded);
    }

    private SharedSecretKeyProducer getEphemeralSharedSecretKeyProducer() throws GeneralSecurityException {
        final byte[] ephemeralKeyEncoded = getEphemeralKeyEncoded();
        final SshEd25519DerivedKey ephemeralDerivedKey = new SshEd25519DerivedKey(ephemeralKeyEncoded);
        final PrivateKey ephemeralPrivateKey = keyConverter.getPrivateKey(ephemeralDerivedKey);
        return new X25519SharedSecretKeyProducer(ephemeralPrivateKey, keyAgreementFactory);
    }

    private byte[] getEphemeralKeyEncoded() {
        final byte[] ephemeralPrivateKeyEncoded = new byte[EllipticCurveKeyType.X25519.getKeyLength()];
        SECURE_RANDOM.nextBytes(ephemeralPrivateKeyEncoded);
        return ephemeralPrivateKeyEncoded;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy