LCOV - code coverage report
Current view: top level - gpg/server - GpgKeys.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 85 96 88.5 %
Date: 2022-11-19 15:00:39 Functions: 16 17 94.1 %

          Line data    Source code
       1             : // Copyright (C) 2015 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.gpg.server;
      16             : 
      17             : import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
      18             : import static java.nio.charset.StandardCharsets.UTF_8;
      19             : 
      20             : import com.google.common.base.CharMatcher;
      21             : import com.google.common.collect.ImmutableList;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.common.io.BaseEncoding;
      24             : import com.google.gerrit.extensions.common.GpgKeyInfo;
      25             : import com.google.gerrit.extensions.registration.DynamicMap;
      26             : import com.google.gerrit.extensions.restapi.AuthException;
      27             : import com.google.gerrit.extensions.restapi.ChildCollection;
      28             : import com.google.gerrit.extensions.restapi.IdString;
      29             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      30             : import com.google.gerrit.extensions.restapi.Response;
      31             : import com.google.gerrit.extensions.restapi.RestReadView;
      32             : import com.google.gerrit.extensions.restapi.RestView;
      33             : import com.google.gerrit.gpg.BouncyCastleUtil;
      34             : import com.google.gerrit.gpg.CheckResult;
      35             : import com.google.gerrit.gpg.Fingerprint;
      36             : import com.google.gerrit.gpg.GerritPublicKeyChecker;
      37             : import com.google.gerrit.gpg.PublicKeyChecker;
      38             : import com.google.gerrit.gpg.PublicKeyStore;
      39             : import com.google.gerrit.server.CurrentUser;
      40             : import com.google.gerrit.server.account.AccountResource;
      41             : import com.google.gerrit.server.account.externalids.ExternalId;
      42             : import com.google.gerrit.server.account.externalids.ExternalIds;
      43             : import com.google.inject.Inject;
      44             : import com.google.inject.Provider;
      45             : import com.google.inject.Singleton;
      46             : import java.io.ByteArrayOutputStream;
      47             : import java.io.IOException;
      48             : import java.util.Arrays;
      49             : import java.util.HashMap;
      50             : import java.util.Iterator;
      51             : import java.util.Map;
      52             : import org.bouncycastle.bcpg.ArmoredOutputStream;
      53             : import org.bouncycastle.openpgp.PGPException;
      54             : import org.bouncycastle.openpgp.PGPPublicKey;
      55             : import org.bouncycastle.openpgp.PGPPublicKeyRing;
      56             : import org.eclipse.jgit.util.NB;
      57             : 
      58             : @Singleton
      59             : public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
      60           7 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      61             : 
      62             :   private final DynamicMap<RestView<GpgKey>> views;
      63             :   private final Provider<CurrentUser> self;
      64             :   private final Provider<PublicKeyStore> storeProvider;
      65             :   private final GerritPublicKeyChecker.Factory checkerFactory;
      66             :   private final ExternalIds externalIds;
      67             : 
      68             :   @Inject
      69             :   GpgKeys(
      70             :       DynamicMap<RestView<GpgKey>> views,
      71             :       Provider<CurrentUser> self,
      72             :       Provider<PublicKeyStore> storeProvider,
      73             :       GerritPublicKeyChecker.Factory checkerFactory,
      74           7 :       ExternalIds externalIds) {
      75           7 :     this.views = views;
      76           7 :     this.self = self;
      77           7 :     this.storeProvider = storeProvider;
      78           7 :     this.checkerFactory = checkerFactory;
      79           7 :     this.externalIds = externalIds;
      80           7 :   }
      81             : 
      82             :   @Override
      83             :   public ListGpgKeys list() throws ResourceNotFoundException, AuthException {
      84           1 :     return new ListGpgKeys();
      85             :   }
      86             : 
      87             :   @Override
      88             :   public GpgKey parse(AccountResource parent, IdString id)
      89             :       throws ResourceNotFoundException, PGPException, IOException {
      90           2 :     checkVisible(self, parent);
      91             : 
      92           2 :     ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent));
      93           2 :     byte[] fp = parseFingerprint(gpgKeyExtId);
      94           2 :     try (PublicKeyStore store = storeProvider.get()) {
      95           2 :       long keyId = keyId(fp);
      96           2 :       for (PGPPublicKeyRing keyRing : store.get(keyId)) {
      97           2 :         PGPPublicKey key = keyRing.getPublicKey();
      98           2 :         if (Arrays.equals(key.getFingerprint(), fp)) {
      99           2 :           return new GpgKey(parent.getUser(), keyRing);
     100             :         }
     101           0 :       }
     102           2 :     }
     103             : 
     104           0 :     throw new ResourceNotFoundException(id);
     105             :   }
     106             : 
     107             :   static ExternalId findGpgKey(String str, Iterable<ExternalId> existingExtIds)
     108             :       throws ResourceNotFoundException {
     109           2 :     str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
     110           2 :     if ((str.length() != 8 && str.length() != 40)
     111           2 :         || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
     112           0 :       throw new ResourceNotFoundException(str);
     113             :     }
     114           2 :     ExternalId gpgKeyExtId = null;
     115           2 :     for (ExternalId extId : existingExtIds) {
     116           2 :       String fpStr = extId.key().id();
     117           2 :       if (!fpStr.endsWith(str)) {
     118           1 :         continue;
     119           2 :       } else if (gpgKeyExtId != null) {
     120           0 :         throw new ResourceNotFoundException("Multiple keys found for " + str);
     121             :       }
     122           2 :       gpgKeyExtId = extId;
     123           2 :       if (str.length() == 40) {
     124           1 :         break;
     125             :       }
     126           2 :     }
     127           2 :     if (gpgKeyExtId == null) {
     128           1 :       throw new ResourceNotFoundException(str);
     129             :     }
     130           2 :     return gpgKeyExtId;
     131             :   }
     132             : 
     133             :   static byte[] parseFingerprint(ExternalId gpgKeyExtId) {
     134           2 :     return BaseEncoding.base16().decode(gpgKeyExtId.key().id());
     135             :   }
     136             : 
     137             :   @Override
     138             :   public DynamicMap<RestView<GpgKey>> views() {
     139           1 :     return views;
     140             :   }
     141             : 
     142           1 :   public class ListGpgKeys implements RestReadView<AccountResource> {
     143             :     @Override
     144             :     public Response<Map<String, GpgKeyInfo>> apply(AccountResource rsrc)
     145             :         throws PGPException, IOException, ResourceNotFoundException {
     146           1 :       checkVisible(self, rsrc);
     147           1 :       Map<String, GpgKeyInfo> keys = new HashMap<>();
     148           1 :       try (PublicKeyStore store = storeProvider.get()) {
     149           1 :         for (ExternalId extId : getGpgExtIds(rsrc)) {
     150           1 :           byte[] fp = parseFingerprint(extId);
     151           1 :           boolean found = false;
     152           1 :           for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
     153           1 :             if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
     154           1 :               found = true;
     155           1 :               GpgKeyInfo info =
     156           1 :                   toJson(
     157           1 :                       keyRing.getPublicKey(), checkerFactory.create(rsrc.getUser(), store), store);
     158           1 :               keys.put(info.id, info);
     159           1 :               info.id = null;
     160           1 :               break;
     161             :             }
     162           0 :           }
     163           1 :           if (!found) {
     164           0 :             logger.atWarning().log(
     165           0 :                 "No public key stored for fingerprint %s", Fingerprint.toString(fp));
     166             :           }
     167           1 :         }
     168             :       }
     169           1 :       return Response.ok(keys);
     170             :     }
     171             :   }
     172             : 
     173             :   @Singleton
     174             :   public static class Get implements RestReadView<GpgKey> {
     175             :     private final Provider<PublicKeyStore> storeProvider;
     176             :     private final GerritPublicKeyChecker.Factory checkerFactory;
     177             : 
     178             :     @Inject
     179           7 :     Get(Provider<PublicKeyStore> storeProvider, GerritPublicKeyChecker.Factory checkerFactory) {
     180           7 :       this.storeProvider = storeProvider;
     181           7 :       this.checkerFactory = checkerFactory;
     182           7 :     }
     183             : 
     184             :     @Override
     185             :     public Response<GpgKeyInfo> apply(GpgKey rsrc) throws IOException {
     186           2 :       try (PublicKeyStore store = storeProvider.get()) {
     187           2 :         return Response.ok(
     188           2 :             toJson(
     189           2 :                 rsrc.getKeyRing().getPublicKey(),
     190           2 :                 checkerFactory.create().setExpectedUser(rsrc.getUser()),
     191             :                 store));
     192             :       }
     193             :     }
     194             :   }
     195             : 
     196             :   private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException {
     197           2 :     return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
     198             :   }
     199             : 
     200             :   private static long keyId(byte[] fp) {
     201           2 :     return NB.decodeInt64(fp, fp.length - 8);
     202             :   }
     203             : 
     204             :   static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc)
     205             :       throws ResourceNotFoundException {
     206           2 :     if (!BouncyCastleUtil.havePGP()) {
     207           0 :       throw new ResourceNotFoundException("GPG not enabled");
     208             :     }
     209           2 :     if (!self.get().hasSameAccountId(rsrc.getUser())) {
     210           1 :       throw new ResourceNotFoundException();
     211             :     }
     212           2 :   }
     213             : 
     214             :   public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult) throws IOException {
     215           2 :     GpgKeyInfo info = new GpgKeyInfo();
     216             : 
     217           2 :     if (key != null) {
     218           2 :       info.id = PublicKeyStore.keyIdToString(key.getKeyID());
     219           2 :       info.fingerprint = Fingerprint.toString(key.getFingerprint());
     220           2 :       Iterator<String> userIds = key.getUserIDs();
     221           2 :       info.userIds = ImmutableList.copyOf(userIds);
     222             : 
     223           2 :       try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096)) {
     224           2 :         try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
     225             :           // This is not exactly the key stored in the store, but is equivalent. In
     226             :           // particular, it will have a Bouncy Castle version string. The armored
     227             :           // stream reader in PublicKeyStore doesn't give us an easy way to extract
     228             :           // the original ASCII armor.
     229           2 :           key.encode(aout);
     230             :         }
     231           2 :         info.key = new String(out.toByteArray(), UTF_8);
     232             :       }
     233             :     }
     234             : 
     235           2 :     info.status = checkResult.getStatus();
     236           2 :     info.problems = checkResult.getProblems();
     237             : 
     238           2 :     return info;
     239             :   }
     240             : 
     241             :   static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker, PublicKeyStore store)
     242             :       throws IOException {
     243           2 :     return toJson(key, checker.setStore(store).check(key));
     244             :   }
     245             : 
     246             :   public static void toJson(GpgKeyInfo info, CheckResult checkResult) {
     247           0 :     info.status = checkResult.getStatus();
     248           0 :     info.problems = checkResult.getProblems();
     249           0 :   }
     250             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750