/** * * Copyright 2017 Paul Schaub * * 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 org.jivesoftware.smackx.omemo; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PRE_KEY_COUNT_PER_BUNDLE; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.SortedSet; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException; import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; import org.jxmpp.jid.BareJid; /** * Class that presents some methods that are used to load/generate/store keys and session data needed for OMEMO. * * @param IdentityKeyPair class * @param IdentityKey class * @param PreKey class * @param SignedPreKey class * @param Session class * @param Address class * @param Elliptic Curve PublicKey class * @param Bundle class * @param Cipher class * @author Paul Schaub */ public abstract class OmemoStore { private static final Logger LOGGER = Logger.getLogger(OmemoStore.class.getName()); /** * Create a new OmemoStore. */ public OmemoStore() { } /** * Returns a sorted set of all the deviceIds, the localUser has had data stored under in the store. * Basically this returns the deviceIds of all "accounts" of localUser, which are known to the store. * @param localUser BareJid of the user. * @return set of deviceIds with available data. */ public abstract SortedSet localDeviceIdsOf(BareJid localUser); /** * Check, if our freshly generated deviceId is available (unique) in our deviceList. * * @param userDevice our current device. * @param id deviceId to check for. * @return true if list did not contain our id, else false * @throws IOException if an I/O error occurred. */ boolean isAvailableDeviceId(OmemoDevice userDevice, int id) throws IOException { LOGGER.log(Level.INFO, "Check if id " + id + " is available..."); // Lookup local cached device list BareJid ownJid = userDevice.getJid(); OmemoCachedDeviceList cachedDeviceList; cachedDeviceList = loadCachedDeviceList(userDevice, ownJid); if (cachedDeviceList == null) { cachedDeviceList = new OmemoCachedDeviceList(); } // Does the list already contain that id? return !cachedDeviceList.contains(id); } /** * Merge the received OmemoDeviceListElement with the one we already have. If we had none, the received one is saved. * * @param userDevice our OmemoDevice. * @param contact Contact we received the list from. * @param list List we received. * @throws IOException if an I/O error occurred. */ OmemoCachedDeviceList mergeCachedDeviceList(OmemoDevice userDevice, BareJid contact, OmemoDeviceListElement list) throws IOException { OmemoCachedDeviceList cached = loadCachedDeviceList(userDevice, contact); if (cached == null) { cached = new OmemoCachedDeviceList(); } if (list == null) { return cached; } for (int devId : list.getDeviceIds()) { if (!cached.contains(devId)) { setDateOfLastDeviceIdPublication(userDevice, new OmemoDevice(contact, devId), new Date()); } } cached.merge(list.getDeviceIds()); storeCachedDeviceList(userDevice, contact, cached); return cached; } /** * Renew our singed preKey. This should be done once every 7-14 days. * The old signed PreKey should be kept for around a month or so (look it up in the XEP). * * @param userDevice our OmemoDevice. * @throws CorruptedOmemoKeyException when our identityKey is invalid. * @throws IOException if an I/O error occurred. * @throws IllegalStateException when our IdentityKeyPair is null. */ void changeSignedPreKey(OmemoDevice userDevice) throws CorruptedOmemoKeyException, IOException { T_IdKeyPair idKeyPair = loadOmemoIdentityKeyPair(userDevice); if (idKeyPair == null) { throw new IllegalStateException("Our IdentityKeyPair is null."); } TreeMap signedPreKeys = loadOmemoSignedPreKeys(userDevice); if (signedPreKeys.size() == 0) { T_SigPreKey newKey = generateOmemoSignedPreKey(idKeyPair, 1); storeOmemoSignedPreKey(userDevice, 1, newKey); } else { int lastId = signedPreKeys.lastKey(); T_SigPreKey newKey = generateOmemoSignedPreKey(idKeyPair, lastId + 1); storeOmemoSignedPreKey(userDevice, lastId + 1, newKey); } setDateOfLastSignedPreKeyRenewal(userDevice, new Date()); removeOldSignedPreKeys(userDevice); } /** * Remove the oldest signedPreKey until there are only MAX_NUMBER_OF_STORED_SIGNED_PREKEYS left. * * @param userDevice our OmemoDevice. * @throws IOException if an I/O error occurred. */ private void removeOldSignedPreKeys(OmemoDevice userDevice) throws IOException { if (OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys() <= 0) { return; } TreeMap signedPreKeys = loadOmemoSignedPreKeys(userDevice); for (int i = 0; i < signedPreKeys.keySet().size() - OmemoConfiguration.getMaxNumberOfStoredSignedPreKeys(); i++) { int keyId = signedPreKeys.firstKey(); LOGGER.log(Level.INFO, "Remove signedPreKey " + keyId + "."); removeOmemoSignedPreKey(userDevice, i); signedPreKeys = loadOmemoSignedPreKeys(userDevice); } } /** * Pack a OmemoBundleElement containing our key material. * * @param userDevice our OmemoDevice. * @return OmemoBundleElement TODO javadoc me please * @throws CorruptedOmemoKeyException when a key could not be loaded * @throws IOException if an I/O error occurred. */ OmemoBundleElement_VAxolotl packOmemoBundle(OmemoDevice userDevice) throws CorruptedOmemoKeyException, IOException { int currentSignedPreKeyId = loadCurrentOmemoSignedPreKeyId(userDevice); T_SigPreKey currentSignedPreKey = loadOmemoSignedPreKeys(userDevice).get(currentSignedPreKeyId); return new OmemoBundleElement_VAxolotl( currentSignedPreKeyId, keyUtil().signedPreKeyPublicForBundle(currentSignedPreKey), keyUtil().signedPreKeySignatureFromKey(currentSignedPreKey), keyUtil().identityKeyForBundle(keyUtil().identityKeyFromPair(loadOmemoIdentityKeyPair(userDevice))), keyUtil().preKeyPublicKeysForBundle(loadOmemoPreKeys(userDevice)) ); } /** * Replenish our supply of keys. If we are missing any type of keys, generate them fresh. * @param userDevice TODO javadoc me please * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted. * @throws IOException if an I/O error occurred. */ public void replenishKeys(OmemoDevice userDevice) throws CorruptedOmemoKeyException, IOException { T_IdKeyPair identityKeyPair = loadOmemoIdentityKeyPair(userDevice); if (identityKeyPair == null) { identityKeyPair = generateOmemoIdentityKeyPair(); storeOmemoIdentityKeyPair(userDevice, identityKeyPair); } TreeMap signedPreKeys = loadOmemoSignedPreKeys(userDevice); if (signedPreKeys.size() == 0) { changeSignedPreKey(userDevice); } TreeMap preKeys = loadOmemoPreKeys(userDevice); int newKeysCount = PRE_KEY_COUNT_PER_BUNDLE - preKeys.size(); int startId = preKeys.size() == 0 ? 0 : preKeys.lastKey(); if (newKeysCount > 0) { TreeMap newKeys = generateOmemoPreKeys(startId + 1, newKeysCount); storeOmemoPreKeys(userDevice, newKeys); } } // *sigh* /** * Generate a new IdentityKeyPair. We should always have only one pair and usually keep this for a long time. * * @return identityKeyPair TODO javadoc me please */ public T_IdKeyPair generateOmemoIdentityKeyPair() { return keyUtil().generateOmemoIdentityKeyPair(); } /** * Load our identityKeyPair from storage. * Return null, if we have no identityKeyPair. * * @param userDevice our OmemoDevice. * @return identityKeyPair TODO javadoc me please * @throws CorruptedOmemoKeyException Thrown, if the stored key is damaged (*hands up* not my fault!) * @throws IOException if an I/O error occurred. */ public abstract T_IdKeyPair loadOmemoIdentityKeyPair(OmemoDevice userDevice) throws CorruptedOmemoKeyException, IOException; /** * Store our identityKeyPair in storage. It would be a cool feature, if the key could be stored in a encrypted * database or something similar. * * @param userDevice our OmemoDevice. * @param identityKeyPair identityKeyPair * @throws IOException if an I/O error occurred. */ public abstract void storeOmemoIdentityKeyPair(OmemoDevice userDevice, T_IdKeyPair identityKeyPair) throws IOException; /** * Remove the identityKeyPair of a user. * @param userDevice our device. */ public abstract void removeOmemoIdentityKeyPair(OmemoDevice userDevice); /** * Load the public identityKey of a device. * * @param userDevice our OmemoDevice. * @param contactsDevice the device of which we want to load the identityKey. * @return identityKey TODO javadoc me please * @throws CorruptedOmemoKeyException when the key in question is corrupted and cant be deserialized. * @throws IOException if an I/O error occurred. */ public abstract T_IdKey loadOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice) throws CorruptedOmemoKeyException, IOException; /** * Store the public identityKey of the device. * * @param userDevice our OmemoDevice. * @param contactsDevice device. * @param contactsKey identityKey belonging to the contactsDevice. * @throws IOException if an I/O error occurred. */ public abstract void storeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice, T_IdKey contactsKey) throws IOException; /** * Removes the identityKey of a device. * * @param userDevice our omemoDevice. * @param contactsDevice device of which we want to delete the identityKey. */ public abstract void removeOmemoIdentityKey(OmemoDevice userDevice, OmemoDevice contactsDevice); /** * Store the number of messages we sent to a device since we last received a message back. * This counter gets reset to 0 whenever we receive a message from the contacts device. * * @param userDevice our omemoDevice. * @param contactsDevice device of which we want to set the message counter. * @param counter counter value. * @throws IOException if an I/O error occurred. */ public abstract void storeOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice, int counter) throws IOException; /** * Return the current value of the message counter. * This counter represents the number of message we sent to the contactsDevice without getting a reply back. * The default value for this counter is 0. * * @param userDevice our omemoDevice * @param contactsDevice device of which we want to get the message counter. * @return counter value. * @throws IOException if an I/O error occurred. */ public abstract int loadOmemoMessageCounter(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException; /** * Set the date of the last message that was received from a device. * * @param userDevice omemoManager of our device. * @param contactsDevice device in question * @param date date of the last received message * @throws IOException if an I/O error occurred. */ public abstract void setDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) throws IOException; /** * Return the date of the last message that was received from device 'from'. * * @param userDevice our OmemoDevice. * @param contactsDevice device in question * @return date if existent, null * @throws IOException if an I/O error occurred. */ public abstract Date getDateOfLastReceivedMessage(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException; /** * Set the date of the last time the deviceId was published. This method only gets called, when the deviceId * was inactive/non-existent before it was published. * * @param userDevice our OmemoDevice * @param contactsDevice OmemoDevice in question * @param date date of the last publication after not being published * @throws IOException if an I/O error occurred. */ public abstract void setDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice, Date date) throws IOException; /** * Return the date of the last time the deviceId was published after previously being not published. * (Point in time, where the status of the deviceId changed from inactive/non-existent to active). * * @param userDevice our OmemoDevice * @param contactsDevice OmemoDevice in question * @return date of the last publication after not being published * @throws IOException if an I/O error occurred. */ public abstract Date getDateOfLastDeviceIdPublication(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException; /** * Set the date of the last time the signed preKey was renewed. * * @param userDevice our OmemoDevice. * @param date date * @throws IOException if an I/O error occurred. */ public abstract void setDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice, Date date) throws IOException; /** * Get the date of the last time the signed preKey was renewed. * * @param userDevice our OmemoDevice. * @return date if existent, otherwise null * @throws IOException if an I/O error occurred. */ public abstract Date getDateOfLastSignedPreKeyRenewal(OmemoDevice userDevice) throws IOException; /** * Generate 'count' new PreKeys beginning with id 'startId'. * These preKeys are published and can be used by contacts to establish sessions with us. * * @param startId start id * @param count how many keys do we want to generate * @return Map of new preKeys */ public TreeMap generateOmemoPreKeys(int startId, int count) { return keyUtil().generateOmemoPreKeys(startId, count); } /** * Load the preKey with id 'preKeyId' from storage. * * @param userDevice our OmemoDevice. * @param preKeyId id of the key to be loaded * @return loaded preKey * @throws IOException if an I/O error occurred. */ public abstract T_PreKey loadOmemoPreKey(OmemoDevice userDevice, int preKeyId) throws IOException; /** * Store a PreKey in storage. * * @param userDevice our OmemoDevice. * @param preKeyId id of the key * @param preKey key * @throws IOException if an I/O error occurred. */ public abstract void storeOmemoPreKey(OmemoDevice userDevice, int preKeyId, T_PreKey preKey) throws IOException; /** * Store a whole bunch of preKeys. * * @param userDevice our OmemoDevice. * @param preKeyHashMap HashMap of preKeys * @throws IOException if an I/O error occurred. */ public void storeOmemoPreKeys(OmemoDevice userDevice, TreeMap preKeyHashMap) throws IOException { for (Map.Entry entry : preKeyHashMap.entrySet()) { storeOmemoPreKey(userDevice, entry.getKey(), entry.getValue()); } } /** * Remove a preKey from storage. This is called, when a contact used one of our preKeys to establish a session * with us. * * @param userDevice our OmemoDevice. * @param preKeyId id of the used key that will be deleted */ public abstract void removeOmemoPreKey(OmemoDevice userDevice, int preKeyId); /** * Return all our current OmemoPreKeys. * * @param userDevice our OmemoDevice. * @return Map containing our preKeys * @throws IOException if an I/O error occurred. */ public abstract TreeMap loadOmemoPreKeys(OmemoDevice userDevice) throws IOException; /** * Return the signedPreKey with the id 'singedPreKeyId'. * * @param userDevice our OmemoDevice. * @param signedPreKeyId id of the key * @return key TODO javadoc me please * @throws IOException if an I/O error occurred. */ public abstract T_SigPreKey loadOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId) throws IOException; public int loadCurrentOmemoSignedPreKeyId(OmemoDevice userDevice) throws IOException { return loadOmemoSignedPreKeys(userDevice).lastKey(); } /** * Load all our signed PreKeys. * * @param userDevice our OmemoDevice. * @return HashMap of our singedPreKeys * @throws IOException if an I/O error occurred. */ public abstract TreeMap loadOmemoSignedPreKeys(OmemoDevice userDevice) throws IOException; /** * Generate a new signed preKey. * * @param identityKeyPair identityKeyPair used to sign the preKey * @param signedPreKeyId id that the preKey will have * @return signedPreKey TODO javadoc me please * @throws CorruptedOmemoKeyException when something goes wrong */ public T_SigPreKey generateOmemoSignedPreKey(T_IdKeyPair identityKeyPair, int signedPreKeyId) throws CorruptedOmemoKeyException { return keyUtil().generateOmemoSignedPreKey(identityKeyPair, signedPreKeyId); } /** * Store a signedPreKey in storage. * * @param userDevice our OmemoDevice. * @param signedPreKeyId id of the signedPreKey * @param signedPreKey the key itself * @throws IOException if an I/O error occurred. */ public abstract void storeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId, T_SigPreKey signedPreKey) throws IOException; /** * Remove a signedPreKey from storage. * * @param userDevice our OmemoDevice. * @param signedPreKeyId id of the key that will be removed */ public abstract void removeOmemoSignedPreKey(OmemoDevice userDevice, int signedPreKeyId); /** * Load the crypto-lib specific session object of the device from storage. * * @param userDevice our OmemoDevice. * @param contactsDevice device whose session we want to load * @return crypto related session * @throws IOException if an I/O error occurred. */ public abstract T_Sess loadRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice) throws IOException; /** * Load all crypto-lib specific session objects of contact 'contact'. * * @param userDevice our OmemoDevice. * @param contact BareJid of the contact we want to get all sessions from * @return TreeMap of deviceId and sessions of the contact * @throws IOException if an I/O error occurred. */ public abstract HashMap loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException; /** * Store a crypto-lib specific session to storage. * * @param userDevice our OmemoDevice. * @param contactsDevice OmemoDevice whose session we want to store * @param session session * @throws IOException if an I/O error occurred. */ public abstract void storeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice, T_Sess session) throws IOException; /** * Remove a crypto-lib specific session from storage. * * @param userDevice our OmemoDevice. * @param contactsDevice device whose session we want to delete */ public abstract void removeRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice); /** * Remove all crypto-lib specific session of a contact. * * @param userDevice our OmemoDevice. * @param contact BareJid of the contact */ public abstract void removeAllRawSessionsOf(OmemoDevice userDevice, BareJid contact); /** * Return true, if we have a session with the device, otherwise false. * Hint for Signal: Do not try 'return getSession() != null' since this will create a new session. * * @param userDevice our OmemoDevice. * @param contactsDevice device * @return true if we have session, otherwise false */ public abstract boolean containsRawSession(OmemoDevice userDevice, OmemoDevice contactsDevice); /** * Load a list of deviceIds from contact 'contact' from the local cache. * * @param userDevice our OmemoDevice. * @param contact contact we want to get the deviceList of * @return CachedDeviceList of the contact * @throws IOException if an I/O error occurred. */ public abstract OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice, BareJid contact) throws IOException; /** * Load a list of deviceIds from our own devices. * * @param userDevice TODO javadoc me please * @return the cached OMEMO device list. * @throws IOException if an I/O error occurred. */ public OmemoCachedDeviceList loadCachedDeviceList(OmemoDevice userDevice) throws IOException { return loadCachedDeviceList(userDevice, userDevice.getJid()); } /** * Store the DeviceList of the contact in local storage. * See this as a cache. * * @param userDevice our OmemoDevice. * @param contact Contact * @param contactsDeviceList list of the contacts devices' ids. * @throws IOException if an I/O error occurred. */ public abstract void storeCachedDeviceList(OmemoDevice userDevice, BareJid contact, OmemoCachedDeviceList contactsDeviceList) throws IOException; /** * Delete this device's IdentityKey, PreKeys, SignedPreKeys and Sessions. * * @param userDevice our OmemoDevice. */ public abstract void purgeOwnDeviceKeys(OmemoDevice userDevice); /** * Return a concrete KeyUtil object that we can use as a utility to create keys etc. * * @return KeyUtil object */ public abstract OmemoKeyUtil keyUtil(); /** * Return our identityKeys fingerprint. * * @param userDevice our OmemoDevice. * @return fingerprint of our identityKeyPair * * @throws CorruptedOmemoKeyException if the identityKey of userDevice is corrupted. * @throws IOException if an I/O error occurred. */ public OmemoFingerprint getFingerprint(OmemoDevice userDevice) throws CorruptedOmemoKeyException, IOException { T_IdKeyPair keyPair = loadOmemoIdentityKeyPair(userDevice); if (keyPair == null) { return null; } return keyUtil().getFingerprintOfIdentityKey(keyUtil().identityKeyFromPair(keyPair)); } /** * Return the fingerprint of the identityKey belonging to contactsDevice. * * @param userDevice our OmemoDevice. * @param contactsDevice OmemoDevice we want to have the fingerprint for. * @return fingerprint of the userDevices IdentityKey. * @throws CorruptedOmemoKeyException if the IdentityKey is corrupted. * @throws NoIdentityKeyException if no IdentityKey for contactsDevice has been found locally. * @throws IOException if an I/O error occurred. */ public OmemoFingerprint getFingerprint(OmemoDevice userDevice, OmemoDevice contactsDevice) throws CorruptedOmemoKeyException, NoIdentityKeyException, IOException { T_IdKey identityKey = loadOmemoIdentityKey(userDevice, contactsDevice); if (identityKey == null) { throw new NoIdentityKeyException(contactsDevice); } return keyUtil().getFingerprintOfIdentityKey(identityKey); } /** * Return the fingerprint of the given devices announced identityKey. * If we have no local copy of the identityKey of the contact, build a fresh session in order to get the key. * * @param managerGuard authenticated OmemoManager * @param contactsDevice OmemoDevice we want to get the fingerprint from * @return fingerprint TODO javadoc me please * * @throws CannotEstablishOmemoSessionException If we have no local copy of the identityKey of the contact * and are unable to build a fresh session * @throws CorruptedOmemoKeyException If the identityKey we have of the contact is corrupted * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws IOException if an I/O error occurred. */ public OmemoFingerprint getFingerprintAndMaybeBuildSession(OmemoManager.LoggedInOmemoManager managerGuard, OmemoDevice contactsDevice) throws CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, IOException { OmemoManager omemoManager = managerGuard.get(); // Load identityKey T_IdKey identityKey = loadOmemoIdentityKey(omemoManager.getOwnDevice(), contactsDevice); if (identityKey == null) { // Key cannot be loaded. Maybe it doesn't exist. Fetch a bundle to get it... OmemoService.getInstance().buildFreshSessionWithDevice(omemoManager.getConnection(), omemoManager.getOwnDevice(), contactsDevice); } // Load identityKey again identityKey = loadOmemoIdentityKey(omemoManager.getOwnDevice(), contactsDevice); if (identityKey == null) { return null; } return keyUtil().getFingerprintOfIdentityKey(identityKey); } }