001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.signature.consumer;
006
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.List;
010import java.util.NoSuchElementException;
011import java.util.regex.Matcher;
012import java.util.regex.Pattern;
013import javax.annotation.Nullable;
014
015import org.bouncycastle.bcpg.sig.NotationData;
016import org.bouncycastle.openpgp.PGPException;
017import org.bouncycastle.openpgp.PGPPublicKey;
018import org.bouncycastle.openpgp.PGPSecretKey;
019import org.bouncycastle.openpgp.PGPSecretKeyRing;
020import org.bouncycastle.openpgp.PGPSignature;
021import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
022import org.pgpainless.PGPainless;
023import org.pgpainless.key.info.KeyRingInfo;
024import org.pgpainless.key.protection.SecretKeyRingProtector;
025import org.pgpainless.signature.builder.DirectKeySignatureBuilder;
026import org.pgpainless.signature.builder.SelfSignatureBuilder;
027
028public class ProofUtil {
029
030    public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, Proof proof)
031            throws PGPException {
032        return addProofs(secretKey, protector, Collections.singletonList(proof));
033    }
034
035    public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, List<Proof> proofs)
036            throws PGPException {
037        KeyRingInfo info = PGPainless.inspectKeyRing(secretKey);
038        return addProofs(secretKey, protector, info.getPrimaryUserId(), proofs);
039    }
040
041    public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, String userId, Proof proof)
042            throws PGPException {
043        return addProofs(secretKey, protector, userId, Collections.singletonList(proof));
044    }
045
046    public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector,
047                          @Nullable String userId, List<Proof> proofs)
048            throws PGPException {
049        KeyRingInfo info = PGPainless.inspectKeyRing(secretKey);
050        PGPSecretKey certificationKey = secretKey.getSecretKey();
051        PGPPublicKey certificationPubKey = certificationKey.getPublicKey();
052        PGPSignature certification = null;
053
054        // null userid -> make direct key sig
055        if (userId == null) {
056            PGPSignature previousCertification = info.getLatestDirectKeySelfSignature();
057            if (previousCertification == null) {
058                throw new NoSuchElementException("No previous valid direct key signature found.");
059            }
060
061            DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(certificationKey, protector, previousCertification);
062            for (Proof proof : proofs) {
063                sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue());
064            }
065            certification = sigBuilder.build(certificationPubKey);
066            certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, certification);
067        } else {
068            if (!info.isUserIdValid(userId)) {
069                throw new IllegalArgumentException("User ID " + userId + " seems to not be valid for this key.");
070            }
071            PGPSignature previousCertification = info.getLatestUserIdCertification(userId);
072            if (previousCertification == null) {
073                throw new NoSuchElementException("No previous valid user-id certification found.");
074            }
075
076            SelfSignatureBuilder sigBuilder = new SelfSignatureBuilder(certificationKey, protector, previousCertification);
077            for (Proof proof : proofs) {
078                sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue());
079            }
080            certification = sigBuilder.build(certificationPubKey, userId);
081            certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, userId, certification);
082        }
083        certificationKey = PGPSecretKey.replacePublicKey(certificationKey, certificationPubKey);
084        secretKey = PGPSecretKeyRing.insertSecretKey(secretKey, certificationKey);
085
086        return secretKey;
087    }
088
089    public static class Proof {
090        public static final String NOTATION_NAME = "proof@metacode.biz";
091        private final String notationValue;
092
093        public Proof(String notationValue) {
094            if (notationValue == null) {
095                throw new IllegalArgumentException("Notation value cannot be null.");
096            }
097            String trimmed = notationValue.trim();
098            if (trimmed.isEmpty()) {
099                throw new IllegalArgumentException("Notation value cannot be empty.");
100            }
101            this.notationValue = trimmed;
102        }
103
104        public String getNotationName() {
105            return NOTATION_NAME;
106        }
107
108        public String getNotationValue() {
109            return notationValue;
110        }
111
112        public static Proof fromMatrixPermalink(String username, String eventPermalink) {
113            Pattern pattern = Pattern.compile("^https:\\/\\/matrix\\.to\\/#\\/(![a-zA-Z]{18}:matrix\\.org)\\/(\\$[a-zA-Z0-9\\-_]{43})\\?via=.*$");
114            Matcher matcher = pattern.matcher(eventPermalink);
115            if (!matcher.matches()) {
116                throw new IllegalArgumentException("Invalid matrix event permalink.");
117            }
118            String roomId = matcher.group(1);
119            String eventId = matcher.group(2);
120            return new Proof(String.format("matrix:u/%s?org.keyoxide.r=%s&org.keyoxide.e=%s", username, roomId, eventId));
121        }
122
123        @Override
124        public String toString() {
125            return getNotationName() + "=" + getNotationValue();
126        }
127    }
128
129    public static List<Proof> getProofs(PGPSignature signature) {
130        PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets();
131        NotationData[] notations = hashedSubpackets.getNotationDataOccurrences();
132
133        List<Proof> proofs = new ArrayList<>();
134        for (NotationData notation : notations) {
135            if (notation.getNotationName().equals(Proof.NOTATION_NAME)) {
136                proofs.add(new Proof(notation.getNotationValue()));
137            }
138        }
139        return proofs;
140    }
141}