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; 034 035import org.jivesoftware.smackx.ox.element.PubkeyElement; 036import org.jivesoftware.smackx.ox.element.PublicKeysListElement; 037import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException; 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.key.info.KeyRingInfo; 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 TODO javadoc me please 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 PGPPublicKeyRingCollection announcedKeysCollection = null; 111 for (OpenPgpV4Fingerprint announcedFingerprint : announced.keySet()) { 112 PGPPublicKeyRing ring = anyKeys.getPublicKeyRing(announcedFingerprint.getKeyId()); 113 114 if (ring == null) continue; 115 116 if (!new KeyRingInfo(ring).isUserIdValid("xmpp:" + getJid().toString())) { 117 LOGGER.log(Level.WARNING, "Ignore key " + Long.toHexString(ring.getPublicKey().getKeyID()) + 118 " as it lacks the user-id \"xmpp" + getJid().toString() + "\""); 119 continue; 120 } 121 122 if (announcedKeysCollection == null) { 123 announcedKeysCollection = new PGPPublicKeyRingCollection(Collections.singleton(ring)); 124 } else { 125 announcedKeysCollection = PGPPublicKeyRingCollection.addPublicKeyRing(announcedKeysCollection, ring); 126 } 127 } 128 129 return announcedKeysCollection; 130 } 131 132 /** 133 * Return a {@link PGPPublicKeyRingCollection}, which contains all keys from {@code keys}, which are marked with the 134 * {@link OpenPgpTrustStore.Trust} state of {@code trust}. 135 * 136 * @param keys {@link PGPPublicKeyRingCollection} 137 * @param trust {@link OpenPgpTrustStore.Trust} 138 * 139 * @return all keys from {@code keys} with trust state {@code trust}. 140 * 141 * @throws IOException IO error 142 */ 143 protected PGPPublicKeyRingCollection getPublicKeysOfTrustState(PGPPublicKeyRingCollection keys, 144 OpenPgpTrustStore.Trust trust) 145 throws IOException { 146 147 if (keys == null) { 148 return null; 149 } 150 151 Set<PGPPublicKeyRing> toRemove = new HashSet<>(); 152 Iterator<PGPPublicKeyRing> iterator = keys.iterator(); 153 while (iterator.hasNext()) { 154 PGPPublicKeyRing ring = iterator.next(); 155 OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(ring); 156 if (store.getTrust(getJid(), fingerprint) != trust) { 157 toRemove.add(ring); 158 } 159 } 160 161 for (PGPPublicKeyRing ring : toRemove) { 162 keys = PGPPublicKeyRingCollection.removePublicKeyRing(keys, ring); 163 } 164 165 if (!keys.iterator().hasNext()) { 166 return null; 167 } 168 169 return keys; 170 } 171 172 /** 173 * Return a {@link PGPPublicKeyRingCollection} which contains all public keys of the contact, which are announced, 174 * as well as marked as {@link OpenPgpStore.Trust#trusted}. 175 * 176 * @return announced, trusted keys. 177 * 178 * @throws IOException IO error 179 * @throws PGPException PGP error 180 */ 181 public PGPPublicKeyRingCollection getTrustedAnnouncedKeys() 182 throws IOException, PGPException { 183 PGPPublicKeyRingCollection announced = getAnnouncedPublicKeys(); 184 PGPPublicKeyRingCollection trusted = getPublicKeysOfTrustState(announced, OpenPgpTrustStore.Trust.trusted); 185 return trusted; 186 } 187 188 /** 189 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state 190 * {@link OpenPgpStore.Trust#trusted}. 191 * 192 * @return trusted fingerprints 193 * 194 * @throws IOException IO error 195 * @throws PGPException PGP error 196 */ 197 public Set<OpenPgpV4Fingerprint> getTrustedFingerprints() 198 throws IOException, PGPException { 199 return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.trusted); 200 } 201 202 /** 203 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state 204 * {@link OpenPgpStore.Trust#untrusted}. 205 * 206 * @return untrusted fingerprints 207 * 208 * @throws IOException IO error 209 * @throws PGPException PGP error 210 */ 211 public Set<OpenPgpV4Fingerprint> getUntrustedFingerprints() 212 throws IOException, PGPException { 213 return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.untrusted); 214 } 215 216 /** 217 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state 218 * {@link OpenPgpStore.Trust#undecided}. 219 * 220 * @return undecided fingerprints 221 * 222 * @throws IOException IO error 223 * @throws PGPException PGP error 224 */ 225 public Set<OpenPgpV4Fingerprint> getUndecidedFingerprints() 226 throws IOException, PGPException { 227 return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.undecided); 228 } 229 230 /** 231 * Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys in {@code publicKeys}, which are marked with the 232 * {@link OpenPgpTrustStore.Trust} of {@code trust}. 233 * 234 * @param publicKeys {@link PGPPublicKeyRingCollection} of keys which are iterated. 235 * @param trust {@link OpenPgpTrustStore.Trust} state. 236 * @return {@link Set} of fingerprints 237 * 238 * @throws IOException IO error 239 */ 240 public Set<OpenPgpV4Fingerprint> getFingerprintsOfKeysWithState(PGPPublicKeyRingCollection publicKeys, 241 OpenPgpTrustStore.Trust trust) 242 throws IOException { 243 PGPPublicKeyRingCollection keys = getPublicKeysOfTrustState(publicKeys, trust); 244 Set<OpenPgpV4Fingerprint> fingerprints = new HashSet<>(); 245 246 if (keys == null) { 247 return fingerprints; 248 } 249 250 for (PGPPublicKeyRing ring : keys) { 251 fingerprints.add(new OpenPgpV4Fingerprint(ring)); 252 } 253 254 return fingerprints; 255 } 256 257 /** 258 * Determine the {@link OpenPgpTrustStore.Trust} state of the key identified by the {@code fingerprint}. 259 * 260 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key 261 * @return trust record 262 * 263 * @throws IOException IO error 264 */ 265 public OpenPgpTrustStore.Trust getTrust(OpenPgpV4Fingerprint fingerprint) 266 throws IOException { 267 return store.getTrust(getJid(), fingerprint); 268 } 269 270 /** 271 * Determine, whether the key identified by the {@code fingerprint} is marked as 272 * {@link OpenPgpTrustStore.Trust#trusted} or not. 273 * 274 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key 275 * @return true, if the key is marked as trusted, false otherwise 276 * 277 * @throws IOException IO error 278 */ 279 public boolean isTrusted(OpenPgpV4Fingerprint fingerprint) 280 throws IOException { 281 return getTrust(fingerprint) == OpenPgpTrustStore.Trust.trusted; 282 } 283 284 /** 285 * Mark a key as {@link OpenPgpStore.Trust#trusted}. 286 * 287 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key to mark as trusted. 288 * 289 * @throws IOException IO error 290 */ 291 public void trust(OpenPgpV4Fingerprint fingerprint) 292 throws IOException { 293 store.setTrust(getJid(), fingerprint, OpenPgpTrustStore.Trust.trusted); 294 } 295 296 /** 297 * Mark a key as {@link OpenPgpStore.Trust#untrusted}. 298 * 299 * @param fingerprint {@link OpenPgpV4Fingerprint} of the key to mark as untrusted. 300 * 301 * @throws IOException IO error 302 */ 303 public void distrust(OpenPgpV4Fingerprint fingerprint) 304 throws IOException { 305 store.setTrust(getJid(), fingerprint, OpenPgpTrustStore.Trust.untrusted); 306 } 307 308 /** 309 * Determine, whether there are keys available, for which we did not yet decided whether to trust them or not. 310 * 311 * @return more than 0 keys with trust state {@link OpenPgpTrustStore.Trust#undecided}. 312 * 313 * @throws IOException I/O error reading the keys or trust records. 314 * @throws PGPException PGP error reading the keys. 315 */ 316 public boolean hasUndecidedKeys() 317 throws IOException, PGPException { 318 return getUndecidedFingerprints().size() != 0; 319 } 320 321 /** 322 * Return a {@link Map} of any unfetchable keys fingerprints and the cause of them not being fetched. 323 * 324 * @return unfetchable keys 325 */ 326 public Map<OpenPgpV4Fingerprint, Throwable> getUnfetchableKeys() { 327 return new HashMap<>(unfetchableKeys); 328 } 329 330 /** 331 * Update the contacts keys by consulting the users PubSub nodes. 332 * This method fetches the users metadata node and then tries to fetch any announced keys. 333 * 334 * @param connection our {@link XMPPConnection}. 335 * 336 * @throws InterruptedException In case the thread gets interrupted. 337 * @throws SmackException.NotConnectedException in case the connection is not connected. 338 * @throws SmackException.NoResponseException in case the server doesn't respond. 339 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 340 * @throws PubSubException.NotALeafNodeException in case the metadata node is not a {@link LeafNode}. 341 * @throws PubSubException.NotAPubSubNodeException in case the metadata node is not a PubSub node. 342 * @throws IOException IO is brittle. 343 */ 344 public void updateKeys(XMPPConnection connection) throws InterruptedException, SmackException.NotConnectedException, 345 SmackException.NoResponseException, XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, 346 PubSubException.NotAPubSubNodeException, IOException { 347 PublicKeysListElement metadata = OpenPgpPubSubUtil.fetchPubkeysList(connection, getJid()); 348 if (metadata == null) { 349 return; 350 } 351 352 updateKeys(connection, metadata); 353 } 354 355 /** 356 * Update the contacts keys using a prefetched {@link PublicKeysListElement}. 357 * 358 * @param connection our {@link XMPPConnection}. 359 * @param metadata pre-fetched OX metadata node of the contact. 360 * 361 * @throws InterruptedException in case the thread gets interrupted. 362 * @throws SmackException.NotConnectedException in case the connection is not connected. 363 * @throws SmackException.NoResponseException in case the server doesn't respond. 364 * @throws IOException IO is dangerous. 365 */ 366 @SuppressWarnings("JavaUtilDate") 367 public void updateKeys(XMPPConnection connection, PublicKeysListElement metadata) 368 throws InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, 369 IOException { 370 371 Map<OpenPgpV4Fingerprint, Date> fingerprintsAndDates = new HashMap<>(); 372 for (OpenPgpV4Fingerprint fingerprint : metadata.getMetadata().keySet()) { 373 fingerprintsAndDates.put(fingerprint, metadata.getMetadata().get(fingerprint).getDate()); 374 } 375 376 store.setAnnouncedFingerprintsOf(getJid(), fingerprintsAndDates); 377 Map<OpenPgpV4Fingerprint, Date> fetchDates = store.getPublicKeyFetchDates(getJid()); 378 379 for (OpenPgpV4Fingerprint fingerprint : metadata.getMetadata().keySet()) { 380 Date fetchDate = fetchDates.get(fingerprint); 381 if (fetchDate != null && fingerprintsAndDates.get(fingerprint) != null && fetchDate.after(fingerprintsAndDates.get(fingerprint))) { 382 LOGGER.log(Level.FINE, "Skip key " + Long.toHexString(fingerprint.getKeyId()) + " as we already have the most recent version. " + 383 "Last announced: " + fingerprintsAndDates.get(fingerprint).toString() + " Last fetched: " + fetchDate.toString()); 384 continue; 385 } 386 try { 387 PubkeyElement key = OpenPgpPubSubUtil.fetchPubkey(connection, getJid(), fingerprint); 388 unfetchableKeys.remove(fingerprint); 389 fetchDates.put(fingerprint, new Date()); 390 if (key == null) { 391 LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) + 392 " can not be imported: Is null"); 393 unfetchableKeys.put(fingerprint, new NullPointerException("Public key is null.")); 394 continue; 395 } 396 PGPPublicKeyRing keyRing = new PGPPublicKeyRing(Base64.decode(key.getDataElement().getB64Data()), new BcKeyFingerprintCalculator()); 397 store.importPublicKey(getJid(), keyRing); 398 } catch (PubSubException.NotAPubSubNodeException | PubSubException.NotALeafNodeException | 399 XMPPException.XMPPErrorException e) { 400 LOGGER.log(Level.WARNING, "Error fetching public key " + Long.toHexString(fingerprint.getKeyId()), e); 401 unfetchableKeys.put(fingerprint, e); 402 } catch (PGPException | IOException e) { 403 LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) + 404 " can not be imported.", e); 405 unfetchableKeys.put(fingerprint, e); 406 } catch (MissingUserIdOnKeyException e) { 407 LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) + 408 " is missing the user-id \"xmpp:" + getJid() + "\". Refuse to import it.", e); 409 unfetchableKeys.put(fingerprint, e); 410 } 411 } 412 store.setPublicKeyFetchDates(getJid(), fetchDates); 413 } 414}