mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-02 04:39:14 +01:00
resolve review findings
This commit is contained in:
@@ -40,10 +40,13 @@ import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.security.gpg.PublicKeyResource;
|
||||
import sonia.scm.security.gpg.PublicKeyStore;
|
||||
import sonia.scm.security.gpg.RawGpgKey;
|
||||
import sonia.scm.web.EdisonHalAppender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -72,6 +75,9 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
||||
@Inject
|
||||
private ScmPathInfoStore scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
private PublicKeyStore publicKeyStore;
|
||||
|
||||
abstract ContributorDto map(Contributor contributor);
|
||||
|
||||
abstract SignatureDto map(Signature signature);
|
||||
@@ -80,7 +86,8 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
||||
|
||||
@ObjectFactory
|
||||
SignatureDto createDto(Signature signature) {
|
||||
if (signature.getType().equals("gpg")) {
|
||||
final Optional<RawGpgKey> key = publicKeyStore.findById(signature.getKeyId());
|
||||
if (signature.getType().equals("gpg") && key.isPresent()) {
|
||||
final Links.Builder linkBuilder =
|
||||
linkingTo()
|
||||
.single(link("rawKey", new LinkBuilder(scmPathInfoStore.get(), PublicKeyResource.class)
|
||||
|
||||
@@ -24,48 +24,25 @@
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.bcpg.BCPGOutputStream;
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
||||
import org.bouncycastle.openpgp.PGPKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.security.GPG;
|
||||
import sonia.scm.security.PrivateKey;
|
||||
import sonia.scm.security.PublicKey;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -100,7 +77,7 @@ public class DefaultGPG implements GPG {
|
||||
public Optional<PublicKey> findPublicKey(String id) {
|
||||
Optional<RawGpgKey> key = publicKeyStore.findById(id);
|
||||
|
||||
return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()));
|
||||
return key.map(rawGpgKey -> new DefaultPublicKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,7 +87,7 @@ public class DefaultGPG implements GPG {
|
||||
if (!keys.isEmpty()) {
|
||||
return keys
|
||||
.stream()
|
||||
.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()))
|
||||
.map(rawGpgKey -> new DefaultPublicKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@@ -124,98 +101,21 @@ public class DefaultGPG implements GPG {
|
||||
|
||||
if (!privateRawKey.isPresent()) {
|
||||
try {
|
||||
final PGPKeyRingGenerator keyPair = generateKeyPair();
|
||||
final PGPKeyRingGenerator keyPair = GPGKeyPairGenerator.generateKeyPair();
|
||||
|
||||
final String rawPublicKey = exportKeyRing(keyPair.generatePublicKeyRing());
|
||||
final String rawPrivateKey = exportKeyRing(keyPair.generateSecretKeyRing());
|
||||
final String rawPublicKey = GPGKeyExporter.exportKeyRing(keyPair.generatePublicKeyRing());
|
||||
final String rawPrivateKey = GPGKeyExporter.exportKeyRing(keyPair.generateSecretKeyRing());
|
||||
|
||||
privateKeyStore.setForUserId(userId, rawPrivateKey);
|
||||
publicKeyStore.add("Default SCM-Manager Signing Key", userId, rawPublicKey, true);
|
||||
|
||||
return new DefaultPrivateKey(rawPrivateKey);
|
||||
} catch (PGPException | NoSuchAlgorithmException | NoSuchProviderException | IOException e) {
|
||||
throw new IllegalStateException("Private key could not be generated", e);
|
||||
throw new GPGException("Private key could not be generated", e);
|
||||
}
|
||||
} else {
|
||||
return new DefaultPrivateKey(privateRawKey.get());
|
||||
}
|
||||
}
|
||||
|
||||
String exportKeyRing(PGPKeyRing keyRing) throws IOException {
|
||||
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
final ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(byteArrayOutputStream);
|
||||
keyRing.encode(armoredOutputStream);
|
||||
armoredOutputStream.close();
|
||||
return new String(byteArrayOutputStream.toByteArray());
|
||||
}
|
||||
|
||||
PGPKeyRingGenerator generateKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(2048);
|
||||
|
||||
KeyPair pair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
PGPKeyPair keyPair = new JcaPGPKeyPair(PublicKeyAlgorithmTags.RSA_GENERAL, pair, new Date());
|
||||
final User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
|
||||
final Person person = new Person(user.getDisplayName(), user.getMail());
|
||||
|
||||
return new PGPKeyRingGenerator(
|
||||
PGPSignature.POSITIVE_CERTIFICATION,
|
||||
keyPair,
|
||||
person.toString(),
|
||||
new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1),
|
||||
null,
|
||||
null,
|
||||
new JcaPGPContentSignerBuilder(PublicKeyAlgorithmTags.RSA_GENERAL, HashAlgorithmTags.SHA1),
|
||||
new JcePBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256).build(new char[]{})
|
||||
);
|
||||
}
|
||||
|
||||
static class DefaultPrivateKey implements PrivateKey {
|
||||
|
||||
final Optional<PGPPrivateKey> privateKey;
|
||||
|
||||
DefaultPrivateKey(String rawPrivateKey) {
|
||||
privateKey = PgpPrivateKeyExtractor.getFromRawKey(rawPrivateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
if (privateKey.isPresent()) {
|
||||
return Keys.createId(privateKey.get());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] sign(InputStream stream) {
|
||||
|
||||
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
|
||||
new JcaPGPContentSignerBuilder(
|
||||
PublicKeyAlgorithmTags.RSA_GENERAL,
|
||||
HashAlgorithmTags.SHA1).setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
);
|
||||
|
||||
if (privateKey.isPresent()) {
|
||||
try {
|
||||
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey.get());
|
||||
} catch (PGPException e) {
|
||||
throw new IllegalStateException("Could not initialize signature generator", e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Missing private key");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) {
|
||||
signatureGenerator.update(IOUtils.toByteArray(stream));
|
||||
signatureGenerator.generate().encode(out);
|
||||
} catch (PGPException | IOException e) {
|
||||
throw new IllegalStateException("Could not create signature", e);
|
||||
}
|
||||
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.bcpg.BCPGOutputStream;
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
import sonia.scm.security.PrivateKey;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
class DefaultPrivateKey implements PrivateKey {
|
||||
|
||||
final Optional<PGPPrivateKey> privateKey;
|
||||
|
||||
DefaultPrivateKey(String rawPrivateKey) {
|
||||
privateKey = KeysExtractor.extractPrivateKey(rawPrivateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return privateKey.map(Keys::createId).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] sign(InputStream stream) {
|
||||
|
||||
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
|
||||
new JcaPGPContentSignerBuilder(
|
||||
PublicKeyAlgorithmTags.RSA_GENERAL,
|
||||
HashAlgorithmTags.SHA1).setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
);
|
||||
|
||||
if (privateKey.isPresent()) {
|
||||
try {
|
||||
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey.get());
|
||||
} catch (PGPException e) {
|
||||
throw new GPGException("Could not initialize signature generator", e);
|
||||
}
|
||||
} else {
|
||||
throw new GPGException("Missing private key");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) {
|
||||
signatureGenerator.update(IOUtils.toByteArray(stream));
|
||||
signatureGenerator.generate().encode(out);
|
||||
} catch (PGPException | IOException e) {
|
||||
throw new GPGException("Could not create signature", e);
|
||||
}
|
||||
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -46,16 +46,16 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class GpgKey implements PublicKey {
|
||||
public class DefaultPublicKey implements PublicKey {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultPublicKey.class);
|
||||
|
||||
private final String id;
|
||||
private final String owner;
|
||||
private final String raw;
|
||||
private final Set<Person> contacts;
|
||||
|
||||
public GpgKey(String id, String owner, String raw, Set<Person> contacts) {
|
||||
public DefaultPublicKey(String id, String owner, String raw, Set<Person> contacts) {
|
||||
this.id = id;
|
||||
this.owner = owner;
|
||||
this.raw = raw;
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.ContextEntry;
|
||||
|
||||
public final class DeletingReadonlyKeyNotAllowedException extends BadRequestException {
|
||||
|
||||
public DeletingReadonlyKeyNotAllowedException(String keyId) {
|
||||
super(ContextEntry.ContextBuilder.entity(RawGpgKey.class, keyId).build(), "deleting readonly gpg keys is not allowed");
|
||||
}
|
||||
|
||||
private static final String CODE = "3US6mweXy1";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -24,34 +24,20 @@
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.openpgp.PGPKeyRing;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PgpPublicKeyExtractor {
|
||||
class GPGKeyExporter {
|
||||
private GPGKeyExporter() { }
|
||||
|
||||
private PgpPublicKeyExtractor() {}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PgpPublicKeyExtractor.class);
|
||||
|
||||
static Optional<PGPPublicKey> getFromRawKey(String rawKey) {
|
||||
try {
|
||||
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(rawKey.getBytes()));
|
||||
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
|
||||
PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
|
||||
return Optional.of(publicKey);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Invalid PGP key", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
static String exportKeyRing(PGPKeyRing keyRing) throws IOException {
|
||||
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
final ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(byteArrayOutputStream);
|
||||
keyRing.encode(armoredOutputStream);
|
||||
armoredOutputStream.close();
|
||||
return new String(byteArrayOutputStream.toByteArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
||||
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.util.Date;
|
||||
|
||||
class GPGKeyPairGenerator {
|
||||
static PGPKeyRingGenerator generateKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(2048);
|
||||
|
||||
KeyPair pair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
PGPKeyPair keyPair = new JcaPGPKeyPair(PublicKeyAlgorithmTags.RSA_GENERAL, pair, new Date());
|
||||
final User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
|
||||
final Person person = new Person(user.getDisplayName(), user.getMail());
|
||||
|
||||
return new PGPKeyRingGenerator(
|
||||
PGPSignature.POSITIVE_CERTIFICATION,
|
||||
keyPair,
|
||||
person.toString(),
|
||||
new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1),
|
||||
null,
|
||||
null,
|
||||
new JcaPGPContentSignerBuilder(PublicKeyAlgorithmTags.RSA_GENERAL, HashAlgorithmTags.SHA1),
|
||||
new JcePBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256).build(new char[]{})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,24 +24,30 @@
|
||||
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.jcajce.JcaPGPSecretKeyRingCollection;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PgpPrivateKeyExtractor {
|
||||
public class KeysExtractor {
|
||||
|
||||
private PgpPrivateKeyExtractor() {}
|
||||
private KeysExtractor() {}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PgpPrivateKeyExtractor.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(KeysExtractor.class);
|
||||
|
||||
static Optional<PGPPrivateKey> getFromRawKey(String rawKey) {
|
||||
static Optional<PGPPrivateKey> extractPrivateKey(String rawKey) {
|
||||
try (final InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(rawKey.getBytes()))) {
|
||||
JcaPGPSecretKeyRingCollection secretKeyRingCollection = new JcaPGPSecretKeyRingCollection(decoderStream);
|
||||
final PGPPrivateKey privateKey = secretKeyRingCollection.getKeyRings().next().getSecretKey().extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().build(new char[]{}));
|
||||
@@ -51,4 +57,17 @@ public class PgpPrivateKeyExtractor {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
static Optional<PGPPublicKey> extractPublicKey(String rawKey) {
|
||||
try {
|
||||
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(rawKey.getBytes()));
|
||||
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
|
||||
PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
|
||||
return Optional.of(publicKey);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Invalid PGP key", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,7 @@ public class PublicKeyResource {
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = "application/pgp-keys",
|
||||
schema = @Schema(implementation = RawGpgKeyDto.class)
|
||||
mediaType = "application/pgp-keys"
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
package sonia.scm.security.gpg;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.Person;
|
||||
@@ -45,7 +44,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
|
||||
import static sonia.scm.security.gpg.KeysExtractor.extractPublicKey;
|
||||
|
||||
@Singleton
|
||||
public class PublicKeyStore {
|
||||
@@ -85,7 +84,7 @@ public class PublicKeyStore {
|
||||
RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, getContactsFromPublicKey(rawKey), Instant.now(), readonly);
|
||||
|
||||
store.put(master, key);
|
||||
eventBus.post(new PublicKeyCreatedEvent());
|
||||
eventBus.post(new PublicKeyCreatedEvent(new DefaultPublicKey(key.getId(), key.getOwner(), key.getRaw(), key.getContacts())));
|
||||
|
||||
return key;
|
||||
|
||||
@@ -93,7 +92,7 @@ public class PublicKeyStore {
|
||||
|
||||
private Set<Person> getContactsFromPublicKey(String rawKey) {
|
||||
List<String> userIds = new ArrayList<>();
|
||||
Optional<PGPPublicKey> publicKeyFromRawKey = getFromRawKey(rawKey);
|
||||
Optional<PGPPublicKey> publicKeyFromRawKey = extractPublicKey(rawKey);
|
||||
publicKeyFromRawKey.ifPresent(pgpPublicKey -> pgpPublicKey.getUserIDs().forEachRemaining(userIds::add));
|
||||
|
||||
return userIds.stream().map(Person::toPerson).collect(Collectors.toSet());
|
||||
@@ -105,7 +104,7 @@ public class PublicKeyStore {
|
||||
if (!rawGpgKey.isReadonly()) {
|
||||
UserPermissions.changePublicKeys(rawGpgKey.getOwner()).check();
|
||||
store.remove(id);
|
||||
eventBus.post(new PublicKeyDeletedEvent());
|
||||
eventBus.post(new PublicKeyDeletedEvent(new DefaultPublicKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())));
|
||||
} else {
|
||||
throw new DeletingReadonlyKeyNotAllowedException(id);
|
||||
}
|
||||
@@ -129,20 +128,4 @@ public class PublicKeyStore {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("squid:MaximumInheritanceDepth")
|
||||
// exceptions have a deep inheritance depth themselves; therefore we accept this here
|
||||
public static class DeletingReadonlyKeyNotAllowedException extends BadRequestException {
|
||||
|
||||
public DeletingReadonlyKeyNotAllowedException(String keyId) {
|
||||
super(ContextEntry.ContextBuilder.entity(RawGpgKey.class, keyId).build(), "deleting readonly gpg keys is not allowed");
|
||||
}
|
||||
|
||||
private static final String CODE = "3US6mweXy1";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user