diff options
Diffstat (limited to 'java/com/google/gerrit/gpg/server/GpgKeys.java')
-rw-r--r-- | java/com/google/gerrit/gpg/server/GpgKeys.java | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java new file mode 100644 index 0000000000..e555f07554 --- /dev/null +++ b/java/com/google/gerrit/gpg/server/GpgKeys.java @@ -0,0 +1,249 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// 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.google.gerrit.gpg.server; + +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.CharMatcher; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; +import com.google.gerrit.extensions.common.GpgKeyInfo; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ChildCollection; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.gpg.BouncyCastleUtil; +import com.google.gerrit.gpg.CheckResult; +import com.google.gerrit.gpg.Fingerprint; +import com.google.gerrit.gpg.GerritPublicKeyChecker; +import com.google.gerrit.gpg.PublicKeyChecker; +import com.google.gerrit.gpg.PublicKeyStore; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.AccountResource; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.eclipse.jgit.util.NB; + +@Singleton +public class GpgKeys implements ChildCollection<AccountResource, GpgKey> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final DynamicMap<RestView<GpgKey>> views; + private final Provider<CurrentUser> self; + private final Provider<PublicKeyStore> storeProvider; + private final GerritPublicKeyChecker.Factory checkerFactory; + private final ExternalIds externalIds; + + @Inject + GpgKeys( + DynamicMap<RestView<GpgKey>> views, + Provider<CurrentUser> self, + Provider<PublicKeyStore> storeProvider, + GerritPublicKeyChecker.Factory checkerFactory, + ExternalIds externalIds) { + this.views = views; + this.self = self; + this.storeProvider = storeProvider; + this.checkerFactory = checkerFactory; + this.externalIds = externalIds; + } + + @Override + public ListGpgKeys list() throws ResourceNotFoundException, AuthException { + return new ListGpgKeys(); + } + + @Override + public GpgKey parse(AccountResource parent, IdString id) + throws ResourceNotFoundException, PGPException, OrmException, IOException { + checkVisible(self, parent); + + ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent)); + byte[] fp = parseFingerprint(gpgKeyExtId); + try (PublicKeyStore store = storeProvider.get()) { + long keyId = keyId(fp); + for (PGPPublicKeyRing keyRing : store.get(keyId)) { + PGPPublicKey key = keyRing.getPublicKey(); + if (Arrays.equals(key.getFingerprint(), fp)) { + return new GpgKey(parent.getUser(), keyRing); + } + } + } + + throw new ResourceNotFoundException(id); + } + + static ExternalId findGpgKey(String str, Iterable<ExternalId> existingExtIds) + throws ResourceNotFoundException { + str = CharMatcher.whitespace().removeFrom(str).toUpperCase(); + if ((str.length() != 8 && str.length() != 40) + || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) { + throw new ResourceNotFoundException(str); + } + ExternalId gpgKeyExtId = null; + for (ExternalId extId : existingExtIds) { + String fpStr = extId.key().id(); + if (!fpStr.endsWith(str)) { + continue; + } else if (gpgKeyExtId != null) { + throw new ResourceNotFoundException("Multiple keys found for " + str); + } + gpgKeyExtId = extId; + if (str.length() == 40) { + break; + } + } + if (gpgKeyExtId == null) { + throw new ResourceNotFoundException(str); + } + return gpgKeyExtId; + } + + static byte[] parseFingerprint(ExternalId gpgKeyExtId) { + return BaseEncoding.base16().decode(gpgKeyExtId.key().id()); + } + + @Override + public DynamicMap<RestView<GpgKey>> views() { + return views; + } + + public class ListGpgKeys implements RestReadView<AccountResource> { + @Override + public Map<String, GpgKeyInfo> apply(AccountResource rsrc) + throws OrmException, PGPException, IOException, ResourceNotFoundException { + checkVisible(self, rsrc); + Map<String, GpgKeyInfo> keys = new HashMap<>(); + try (PublicKeyStore store = storeProvider.get()) { + for (ExternalId extId : getGpgExtIds(rsrc)) { + byte[] fp = parseFingerprint(extId); + boolean found = false; + for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) { + if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) { + found = true; + GpgKeyInfo info = + toJson( + keyRing.getPublicKey(), checkerFactory.create(rsrc.getUser(), store), store); + keys.put(info.id, info); + info.id = null; + break; + } + } + if (!found) { + logger.atWarning().log( + "No public key stored for fingerprint %s", Fingerprint.toString(fp)); + } + } + } + return keys; + } + } + + @Singleton + public static class Get implements RestReadView<GpgKey> { + private final Provider<PublicKeyStore> storeProvider; + private final GerritPublicKeyChecker.Factory checkerFactory; + + @Inject + Get(Provider<PublicKeyStore> storeProvider, GerritPublicKeyChecker.Factory checkerFactory) { + this.storeProvider = storeProvider; + this.checkerFactory = checkerFactory; + } + + @Override + public GpgKeyInfo apply(GpgKey rsrc) throws IOException { + try (PublicKeyStore store = storeProvider.get()) { + return toJson( + rsrc.getKeyRing().getPublicKey(), + checkerFactory.create().setExpectedUser(rsrc.getUser()), + store); + } + } + } + + private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException { + return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY); + } + + private static long keyId(byte[] fp) { + return NB.decodeInt64(fp, fp.length - 8); + } + + static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc) + throws ResourceNotFoundException { + if (!BouncyCastleUtil.havePGP()) { + throw new ResourceNotFoundException("GPG not enabled"); + } + if (!self.get().hasSameAccountId(rsrc.getUser())) { + throw new ResourceNotFoundException(); + } + } + + public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult) throws IOException { + GpgKeyInfo info = new GpgKeyInfo(); + + if (key != null) { + info.id = PublicKeyStore.keyIdToString(key.getKeyID()); + info.fingerprint = Fingerprint.toString(key.getFingerprint()); + Iterator<String> userIds = key.getUserIDs(); + info.userIds = ImmutableList.copyOf(userIds); + + try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096)) { + try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) { + // This is not exactly the key stored in the store, but is equivalent. In + // particular, it will have a Bouncy Castle version string. The armored + // stream reader in PublicKeyStore doesn't give us an easy way to extract + // the original ASCII armor. + key.encode(aout); + } + info.key = new String(out.toByteArray(), UTF_8); + } + } + + info.status = checkResult.getStatus(); + info.problems = checkResult.getProblems(); + + return info; + } + + static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker, PublicKeyStore store) + throws IOException { + return toJson(key, checker.setStore(store).check(key)); + } + + public static void toJson(GpgKeyInfo info, CheckResult checkResult) { + info.status = checkResult.getStatus(); + info.problems = checkResult.getProblems(); + } +} |