001/** 002 * 003 * Copyright 2018 Paul Schaub. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.ox; 018 019import java.io.IOException; 020import java.util.Collections; 021import java.util.Date; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.Map; 026import java.util.Set; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.jivesoftware.smack.SmackException; 031import org.jivesoftware.smack.XMPPConnection; 032import org.jivesoftware.smack.XMPPException; 033import org.jivesoftware.smack.util.stringencoder.Base64; 034import org.jivesoftware.smackx.ox.element.PubkeyElement; 035import org.jivesoftware.smackx.ox.element.PublicKeysListElement; 036import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException; 037import org.jivesoftware.smackx.ox.selection_strategy.BareJidUserId; 038import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore; 039import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore; 040import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil; 041import org.jivesoftware.smackx.pubsub.LeafNode; 042import org.jivesoftware.smackx.pubsub.PubSubException; 043 044import org.bouncycastle.openpgp.PGPException; 045import org.bouncycastle.openpgp.PGPPublicKeyRing; 046import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; 047import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; 048import org.jxmpp.jid.BareJid; 049import org.pgpainless.key.OpenPgpV4Fingerprint; 050import org.pgpainless.util.BCUtil; 051 052/** 053 * The OpenPgpContact is sort of a specialized view on the OpenPgpStore, which gives you access to the information 054 * about the user. It also allows contact-specific actions like fetching the contacts keys from PubSub etc. 055 */ 056public class OpenPgpContact { 057 058 private final Logger LOGGER; 059 060 protected final BareJid jid; 061 protected final OpenPgpStore store; 062 protected final Map<OpenPgpV4Fingerprint, Throwable> unfetchableKeys = new HashMap<>(); 063 064 /** 065 * Create a new OpenPgpContact. 066 * 067 * @param jid {@link BareJid} of the contact. 068 * @param store {@link OpenPgpStore}. 069 */ 070 public OpenPgpContact(BareJid jid, OpenPgpStore store) { 071 this.jid = jid; 072 this.store = store; 073 LOGGER = Logger.getLogger(OpenPgpContact.class.getName() + ":" + jid.toString()); 074 } 075 076 /** 077 * Return the jid of the contact. 078 * 079 * @return jid 080 */ 081 public BareJid getJid() { 082 return jid; 083 } 084 085 /** 086 * Return any available public keys of the user. The result might also contain outdated or invalid keys. 087 * 088 * @return any keys of the contact. 089 * 090 * @throws IOException IO is dangerous 091 * @throws PGPException PGP is brittle 092 */ 093 public PGPPublicKeyRingCollection getAnyPublicKeys() throws IOException, PGPException { 094 return store.getPublicKeysOf(jid); 095 } 096 097 /** 098 * Return any announced public keys. This is the set returned by {@link #getAnyPublicKeys()} with non-announced 099 * keys and keys which lack a user-id with the contacts jid removed. 100 * 101 * @return announced keys of the contact 102 * 103 * @throws IOException IO is dangerous 104 * @throws PGPException PGP is brittle 105 */ 106 public PGPPublicKeyRingCollection getAnnouncedPublicKeys() throws IOException, PGPException { 107 PGPPublicKeyRingCollection anyKeys = getAnyPublicKeys(); 108 Map<OpenPgpV4Fingerprint, Date> announced = store.getAnnouncedFingerprintsOf(jid); 109 110 BareJidUserId.PubRingSelectionStrategy userIdFilter = new BareJidUserId.PubRingSelectionStrategy(); 111 112 PGPPublicKeyRingCollection announcedKeysCollection = null; 113 for (OpenPgpV4Fingerprint announcedFingerprint : announced.keySet()) { 114 PGPPublicKeyRing ring = anyKeys.getPublicKeyRing(announcedFingerprint.getKeyId()); 115 116 if (ring == null) continue; 117 118 ring = BCUtil.removeUnassociatedKeysFromKeyRing(ring, ring.getPublicKey(announcedFingerprint.getKeyId())); 119 120 if (!userIdFilter.accept(getJid(), ring)) { 121 LOGGER.log(Level.WARNING, "Ignore key " + Long.toHexString(ring.getPublicKey().getKeyID()) + 122 " as it lacks the user-id \"xmpp" + getJid().toString() + "\""); 123 continue; 124 } 125 126 if (announcedKeysCollection == null) { 127 announcedKeysCollection = new PGPPublicKeyRingCollection(Collections.singleton(ring)); 128 } else { 129 announcedKeysCollection = PGPPublicKeyRingCollection.addPublicKeyRing(announcedKeysCollection, ring); 130 } 131 } 132 133 return announcedKeysCollection; 134 } 135 136 /** 137 * Return a {@link PGPPublicKeyRingCollection}, which contains all keys from {@code keys}, which are marked with the 138 * {@link OpenPgpTrustStore.Trust} state of {@code trust}. 139 * 140 * @param keys {@link PGPPublicKeyRingCollection} 141 * @param trust {@link OpenPgpTrustStore.Trust} 142 * 143 * @return all keys from {@code keys} with trust state {@code trust}. 144 * 145 * @throws IOException IO error 146 */ 147 protected PGPPublicKeyRingCollection getPublicKeysOfTrustState(PGPPublicKeyRingCollection keys, 148 OpenPgpTrustStore.Trust trust) 149 throws IOException { 150 151 if (keys == null) { 152 return null; 153 } 154 155 Set<PGPPublicKeyRing> toRemove = new HashSet<>(); 156 Iterator<PGPPublicKeyRing> iterator = keys.iterator(); 157 while (iterator.hasNext()) { 158 PGPPublicKeyRing ring = iterator.next(); 159 OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(ring); 160 if (store.getTrust(getJid(), fingerprint) != trust) { 161 toRemove.add(ring); 162 } 163 } 164 165 for (PGPPublicKeyRing ring : toRemove) { 166 keys = PGPPublicKeyRingCollection.removePublicKeyRing(keys, ring); 167 } 168 169 if (!keys.iterator().hasNext()) { 170 return null; 171 } 172 173 return keys; 174 } 175 176 /** 177 * Return a {@link PGPPublicKeyRingCollection} which contains all public keys of the contact, which are announced, 178 * as well as marked as {@link OpenPgpStore.Trust#trusted}. 179 * 180 * @return announced, trusted keys. 181 * 182 * @throws IOException IO error 183 * @throws PGPException PGP error 184 */ 185 public PGPPublicKeyRingCollection getTrustedAnnouncedKeys() 186 throws IOException, PGPException { 187 PGPPublicKeyRingCollection announced = getAnnouncedPublicKeys(); 188 PGPPublicKeyRingCollection trusted = getPublicKeysOfTrustState(announced, OpenPgpTrustStore.Trust.trusted); 189 return trusted; 190 } 191 192 /** 193 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state 194 * {@link OpenPgpStore.Trust#trusted}. 195 * 196 * @return trusted fingerprints 197 * 198 * @throws IOException IO error 199 * @throws PGPException PGP error 200 */ 201 public Set<OpenPgpV4Fingerprint> getTrustedFingerprints() 202 throws IOException, PGPException { 203 return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.trusted); 204 } 205 206 /** 207 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state 208 * {@link OpenPgpStore.Trust#untrusted}. 209 * 210 * @return untrusted fingerprints 211 * 212 * @throws IOException IO error 213 * @throws PGPException PGP error 214 */ 215 public Set<OpenPgpV4Fingerprint> getUntrustedFingerprints() 216 throws IOException, PGPException { 217 return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.untrusted); 218 } 219 220 /** 221 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state 222 * {@link OpenPgpStore.Trust#undecided}. 223 * 224 * @return undecided fingerprints 225 * 226 * @throws IOException IO error 227 * @throws PGPException PGP error 228 */ 229 public Set<OpenPgpV4Fingerprint> getUndecidedFingerprints() 230 throws IOException, PGPException { 231 return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.undecided); 232 } 233 234 /** 235 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys in {@code publicKeys}, which are marked with the 236 * {@link OpenPgpTrustStore.Trust} of {@code trust}. 237 * 238 * @param publicKeys {@link PGPPublicKeyRingCollection} of keys which are iterated. 239 * @param trust {@link OpenPgpTrustStore.Trust} state. 240 * @return {@link Set} of fingerprints 241 * 242 * @throws IOException IO error 243 */ 244 public Set<OpenPgpV4Fingerprint> getFingerprintsOfKeysWithState(PGPPublicKeyRingCollection publicKeys, 245 OpenPgpTrustStore.Trust trust) 246 throws IOException { 247 PGPPublicKeyRingCollection keys = getPublicKeysOfTrustState(publicKeys, trust); 248 Set<OpenPgpV4Fingerprint> fingerprints = new HashSet<>(); 249 250 if (keys == null) { 251 return fingerprints; 252 } 253 254 for (PGPPublicKeyRing ring : keys) { 255 fingerprints.add(new OpenPgpV4Fingerprint(ring)); 256 } 257 258 return fingerprints; 259 } 260 261 /** 262 * Determine the {@link OpenPgpTrustStore.Trust} state of the key identified by the {@code fingerprint}. 263 * 264 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key 265 * @return trust record 266 * 267 * @throws IOException IO error 268 */ 269 public OpenPgpTrustStore.Trust getTrust(OpenPgpV4Fingerprint fingerprint) 270 throws IOException { 271 return store.getTrust(getJid(), fingerprint); 272 } 273 274 /** 275 * Determine, whether the key identified by the {@code fingerprint} is marked as 276 * {@link OpenPgpTrustStore.Trust#trusted} or not. 277 * 278 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key 279 * @return true, if the key is marked as trusted, false otherwise 280 * 281 * @throws IOException IO error 282 */ 283 public boolean isTrusted(OpenPgpV4Fingerprint fingerprint) 284 throws IOException { 285 return getTrust(fingerprint) == OpenPgpTrustStore.Trust.trusted; 286 } 287 288 /** 289 * Mark a key as {@link OpenPgpStore.Trust#trusted}. 290 * 291 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key to mark as trusted. 292 * 293 * @throws IOException IO error 294 */ 295 public void trust(OpenPgpV4Fingerprint fingerprint) 296 throws IOException { 297 store.setTrust(getJid(), fingerprint, OpenPgpTrustStore.Trust.trusted); 298 } 299 300 /** 301 * Mark a key as {@link OpenPgpStore.Trust#untrusted}. 302 * 303 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key to mark as untrusted. 304 * 305 * @throws IOException IO error 306 */ 307 public void distrust(OpenPgpV4Fingerprint fingerprint) 308 throws IOException { 309 store.setTrust(getJid(), fingerprint, OpenPgpTrustStore.Trust.untrusted); 310 } 311 312 /** 313 * Determine, whether there are keys available, for which we did not yet decided whether to trust them or not. 314 * 315 * @return more than 0 keys with trust state {@link OpenPgpTrustStore.Trust#undecided}. 316 * 317 * @throws IOException I/O error reading the keys or trust records. 318 * @throws PGPException PGP error reading the keys. 319 */ 320 public boolean hasUndecidedKeys() 321 throws IOException, PGPException { 322 return getUndecidedFingerprints().size() != 0; 323 } 324 325 /** 326 * Return a {@link Map} of any unfetchable keys fingerprints and the cause of them not being fetched. 327 * 328 * @return unfetchable keys 329 */ 330 public Map<OpenPgpV4Fingerprint, Throwable> getUnfetchableKeys() { 331 return new HashMap<>(unfetchableKeys); 332 } 333 334 /** 335 * Update the contacts keys by consulting the users PubSub nodes. 336 * This method fetches the users metadata node and then tries to fetch any announced keys. 337 * 338 * @param connection our {@link XMPPConnection}. 339 * 340 * @throws InterruptedException In case the thread gets interrupted. 341 * @throws SmackException.NotConnectedException in case the connection is not connected. 342 * @throws SmackException.NoResponseException in case the server doesn't respond. 343 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 344 * @throws PubSubException.NotALeafNodeException in case the metadata node is not a {@link LeafNode}. 345 * @throws PubSubException.NotAPubSubNodeException in case the metadata node is not a PubSub node. 346 * @throws IOException IO is brittle. 347 */ 348 public void updateKeys(XMPPConnection connection) throws InterruptedException, SmackException.NotConnectedException, 349 SmackException.NoResponseException, XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, 350 PubSubException.NotAPubSubNodeException, IOException { 351 PublicKeysListElement metadata = OpenPgpPubSubUtil.fetchPubkeysList(connection, getJid()); 352 if (metadata == null) { 353 return; 354 } 355 356 updateKeys(connection, metadata); 357 } 358 359 /** 360 * Update the contacts keys using a prefetched {@link PublicKeysListElement}. 361 * 362 * @param connection our {@link XMPPConnection}. 363 * @param metadata pre-fetched OX metadata node of the contact. 364 * 365 * @throws InterruptedException in case the thread gets interrupted. 366 * @throws SmackException.NotConnectedException in case the connection is not connected. 367 * @throws SmackException.NoResponseException in case the server doesn't respond. 368 * @throws IOException IO is dangerous. 369 */ 370 public void updateKeys(XMPPConnection connection, PublicKeysListElement metadata) 371 throws InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, 372 IOException { 373 374 Map<OpenPgpV4Fingerprint, Date> fingerprintsAndDates = new HashMap<>(); 375 for (OpenPgpV4Fingerprint fingerprint : metadata.getMetadata().keySet()) { 376 fingerprintsAndDates.put(fingerprint, metadata.getMetadata().get(fingerprint).getDate()); 377 } 378 379 store.setAnnouncedFingerprintsOf(getJid(), fingerprintsAndDates); 380 Map<OpenPgpV4Fingerprint, Date> fetchDates = store.getPublicKeyFetchDates(getJid()); 381 382 for (OpenPgpV4Fingerprint fingerprint : metadata.getMetadata().keySet()) { 383 Date fetchDate = fetchDates.get(fingerprint); 384 if (fetchDate != null && fingerprintsAndDates.get(fingerprint) != null && fetchDate.after(fingerprintsAndDates.get(fingerprint))) { 385 LOGGER.log(Level.FINE, "Skip key " + Long.toHexString(fingerprint.getKeyId()) + " as we already have the most recent version. " + 386 "Last announced: " + fingerprintsAndDates.get(fingerprint).toString() + " Last fetched: " + fetchDate.toString()); 387 continue; 388 } 389 try { 390 PubkeyElement key = OpenPgpPubSubUtil.fetchPubkey(connection, getJid(), fingerprint); 391 unfetchableKeys.remove(fingerprint); 392 fetchDates.put(fingerprint, new Date()); 393 if (key == null) { 394 LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) + 395 " can not be imported: Is null"); 396 unfetchableKeys.put(fingerprint, new NullPointerException("Public key is null.")); 397 continue; 398 } 399 PGPPublicKeyRing keyRing = new PGPPublicKeyRing(Base64.decode(key.getDataElement().getB64Data()), new BcKeyFingerprintCalculator()); 400 store.importPublicKey(getJid(), keyRing); 401 } catch (PubSubException.NotAPubSubNodeException | PubSubException.NotALeafNodeException | 402 XMPPException.XMPPErrorException e) { 403 LOGGER.log(Level.WARNING, "Error fetching public key " + Long.toHexString(fingerprint.getKeyId()), e); 404 unfetchableKeys.put(fingerprint, e); 405 } catch (PGPException | IOException e) { 406 LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) + 407 " can not be imported.", e); 408 unfetchableKeys.put(fingerprint, e); 409 } catch (MissingUserIdOnKeyException e) { 410 LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) + 411 " is missing the user-id \"xmpp:" + getJid() + "\". Refuse to import it.", e); 412 unfetchableKeys.put(fingerprint, e); 413 } 414 } 415 store.setPublicKeyFetchDates(getJid(), fetchDates); 416 } 417}