blob: 00a0f57ba6becfba2f661d8401a002ee83e47c38 [file] [log] [blame]
// 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();
}
}