252 lines
8.9 KiB
Java
252 lines
8.9 KiB
Java
// 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();
|
|
|
|
public static final String MIME_TYPE = "application/pgp-keys";
|
|
|
|
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);
|
|
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();
|
|
}
|
|
}
|