summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/gpg/server/GpgKeys.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/gpg/server/GpgKeys.java')
-rw-r--r--java/com/google/gerrit/gpg/server/GpgKeys.java249
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();
+ }
+}