|  | // 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.Response; | 
|  | 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.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.Locale; | 
|  | 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, 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(Locale.US); | 
|  | 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 Response<Map<String, GpgKeyInfo>> apply(AccountResource rsrc) | 
|  | throws 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 Response.ok(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 Response<GpgKeyInfo> apply(GpgKey rsrc) throws IOException { | 
|  | try (PublicKeyStore store = storeProvider.get()) { | 
|  | return Response.ok( | 
|  | 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(); | 
|  | } | 
|  | } |