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}