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 : }