001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.generator.gnupg;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.ByteArrayInputStream;
026import java.io.IOException;
027import java.io.UncheckedIOException;
028import java.time.LocalDateTime;
029import java.time.ZoneId;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Iterator;
033import java.util.Map;
034import java.util.function.Predicate;
035
036import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
037import org.bouncycastle.openpgp.PGPException;
038import org.bouncycastle.openpgp.PGPPrivateKey;
039import org.bouncycastle.openpgp.PGPSecretKey;
040import org.bouncycastle.openpgp.PGPSecretKeyRing;
041import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
042import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
043import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
044import org.bouncycastle.openpgp.PGPUtil;
045import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
046import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
047import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
048import org.bouncycastle.util.encoders.Hex;
049import org.eclipse.aether.RepositorySystemSession;
050import org.eclipse.aether.artifact.Artifact;
051import org.eclipse.aether.deployment.DeployRequest;
052import org.eclipse.aether.installation.InstallRequest;
053import org.eclipse.aether.spi.artifact.ArtifactPredicateFactory;
054import org.eclipse.aether.spi.artifact.generator.ArtifactGenerator;
055import org.eclipse.aether.spi.artifact.generator.ArtifactGeneratorFactory;
056import org.eclipse.aether.util.ConfigUtils;
057
058@Singleton
059@Named(GnupgSignatureArtifactGeneratorFactory.NAME)
060public final class GnupgSignatureArtifactGeneratorFactory implements ArtifactGeneratorFactory {
061
062    public interface Loader {
063        /**
064         * Returns the key ring material, or {@code null}.
065         */
066        default byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
067            return null;
068        }
069
070        /**
071         * Returns the key fingerprint, or {@code null}.
072         */
073        default byte[] loadKeyFingerprint(RepositorySystemSession session) throws IOException {
074            return null;
075        }
076
077        /**
078         * Returns the key password, or {@code null}.
079         */
080        default char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
081            return null;
082        }
083    }
084
085    public static final String NAME = "gnupg";
086
087    private final ArtifactPredicateFactory artifactPredicateFactory;
088    private final Map<String, Loader> loaders;
089
090    @Inject
091    public GnupgSignatureArtifactGeneratorFactory(
092            ArtifactPredicateFactory artifactPredicateFactory, Map<String, Loader> loaders) {
093        this.artifactPredicateFactory = artifactPredicateFactory;
094        this.loaders = loaders;
095    }
096
097    @Override
098    public ArtifactGenerator newInstance(RepositorySystemSession session, InstallRequest request) {
099        return null;
100    }
101
102    @Override
103    public ArtifactGenerator newInstance(RepositorySystemSession session, DeployRequest request) {
104        final boolean enabled = ConfigUtils.getBoolean(
105                session, GnupgConfigurationKeys.DEFAULT_ENABLED, GnupgConfigurationKeys.CONFIG_PROP_ENABLED);
106        if (!enabled) {
107            return null;
108        }
109
110        try {
111            return doCreateArtifactGenerator(
112                    session, request.getArtifacts(), artifactPredicateFactory.newInstance(session)::hasChecksums);
113        } catch (IOException e) {
114            throw new UncheckedIOException(e);
115        }
116    }
117
118    @Override
119    public float getPriority() {
120        return 100;
121    }
122
123    private GnupgSignatureArtifactGenerator doCreateArtifactGenerator(
124            RepositorySystemSession session, Collection<Artifact> artifacts, Predicate<Artifact> artifactPredicate)
125            throws IOException {
126
127        byte[] keyRingMaterial = null;
128        for (Loader loader : loaders.values()) {
129            keyRingMaterial = loader.loadKeyRingMaterial(session);
130            if (keyRingMaterial != null) {
131                break;
132            }
133        }
134        if (keyRingMaterial == null) {
135            throw new IllegalArgumentException("Key ring material not found");
136        }
137
138        byte[] fingerprint = null;
139        for (Loader loader : loaders.values()) {
140            fingerprint = loader.loadKeyFingerprint(session);
141            if (fingerprint != null) {
142                break;
143            }
144        }
145
146        try {
147            PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(
148                    PGPUtil.getDecoderStream(new ByteArrayInputStream(keyRingMaterial)),
149                    new BcKeyFingerprintCalculator());
150
151            PGPSecretKey secretKey = null;
152            for (PGPSecretKeyRing ring : pgpSecretKeyRingCollection) {
153                for (PGPSecretKey key : ring) {
154                    if (!key.isPrivateKeyEmpty()) {
155                        if (fingerprint == null || Arrays.equals(fingerprint, key.getFingerprint())) {
156                            secretKey = key;
157                            break;
158                        }
159                    }
160                }
161            }
162            if (secretKey == null) {
163                throw new IllegalArgumentException("Secret key not found");
164            }
165            if (secretKey.isPrivateKeyEmpty()) {
166                throw new IllegalArgumentException("Private key not found in Secret key");
167            }
168
169            long validSeconds = secretKey.getPublicKey().getValidSeconds();
170            if (validSeconds > 0) {
171                LocalDateTime expireDateTime = secretKey
172                        .getPublicKey()
173                        .getCreationTime()
174                        .toInstant()
175                        .atZone(ZoneId.systemDefault())
176                        .toLocalDateTime()
177                        .plusSeconds(validSeconds);
178                if (LocalDateTime.now().isAfter(expireDateTime)) {
179                    throw new IllegalArgumentException("Secret key expired at: " + expireDateTime);
180                }
181            }
182
183            char[] keyPassword = null;
184            final boolean keyPassNeeded = secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithmTags.NULL;
185            if (keyPassNeeded) {
186                for (Loader loader : loaders.values()) {
187                    keyPassword = loader.loadPassword(session, secretKey.getFingerprint());
188                    if (keyPassword != null) {
189                        break;
190                    }
191                }
192                if (keyPassword == null) {
193                    throw new IllegalArgumentException("Secret key is encrypted but no key password provided");
194                }
195            }
196
197            PGPPrivateKey privateKey = secretKey.extractPrivateKey(
198                    new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(keyPassword));
199            if (keyPassword != null) {
200                Arrays.fill(keyPassword, ' ');
201            }
202            PGPSignatureSubpacketGenerator subPacketGenerator = new PGPSignatureSubpacketGenerator();
203            subPacketGenerator.setIssuerFingerprint(false, secretKey);
204            PGPSignatureSubpacketVector hashSubPackets = subPacketGenerator.generate();
205
206            return new GnupgSignatureArtifactGenerator(
207                    artifacts, artifactPredicate, secretKey, privateKey, hashSubPackets, getKeyInfo(secretKey));
208        } catch (PGPException | IOException e) {
209            throw new IllegalStateException(e);
210        }
211    }
212
213    private static String getKeyInfo(PGPSecretKey secretKey) {
214        Iterator<String> userIds = secretKey.getPublicKey().getUserIDs();
215        if (userIds.hasNext()) {
216            return userIds.next();
217        }
218        return Hex.toHexString(secretKey.getPublicKey().getFingerprint());
219    }
220}