Smack OMEMO SQLite Store implementation and OmemoManager#regenerate() support

aTalk has just completed its SQLite store implementation for use in smack Omemo chat support. During the course of testing, need clarifications on some of my observations.

If I performed an omemoManager.regenerate() on an account, I observed the following and with the error log attached below:

#1: Smack OMEMO always generate two deviceId’s, appear one to be new and another previous old deviceId.

#2: on executing the following: fingerprints = mOmemoManager.getActiveFingerprints(bareJid);

it always return an zero-size fingerprints array.

#3 The method IdentityKeyPair loadOmemoIdentityKeyPair(OmemoManager omemoManager)

always return null. I performed a trace and observe that the loadOmemoIdentityKeyPair uses omemoDevice with default deviceId e.g. 394075726;

however the signedPreKeyPairs is only stored only for the second deviceId e.g. 985033729

This problem prevents aTalk to proceed to start an OMEMO chat.

For testing I force the below method when called, also reset the defult deviceID to the omemoManager.getOwnDevice().getDeviceId()

public void storeOmemoSignedPreKey(OmemoManager omemoManager, int signedPreKeyId, SignedPreKeyRecord signedPreKey)

overwritten any value that was saved previously during

public void setDefaultDeviceId(BareJid user, int defaultDeviceId)

#4: If I performed direct deletion of all the OMEMO tables and regenerate new.

I was able to start an OMEMO chat successfully with conversation after some attempts. I need further investigation on this.

==============================================

I reverts OMEMO chat support to using file-based persistent storage and perform the same. The observed behavior is similar to SQLite implementation for the the test cases #1, #2, #3. #4.

For test case #3, I am unable to verify if a wrong deviceId is being used to retrieve the IdentityKeyPair. However the same exception as below is being thrown.

For case #4, deleteted the OMEMO_Store directory helps aTalk able to start OMEMO chat with conversation again.

// ============== Other Observations ==================

#5: The fingerprint returns by aTalk and conversation is different, although both use the same library. Any explanation?

identityKeyPair.getPublicKey().getFingerprint().replaceAll("\s", “”).

FYI: Although different is return string value, both format are working fine with each application.

------ atalk fingerprint ----------

(byte)0x05,(byte)0x80,(byte)0x50,(byte)0x2c,(byte)0xeb,(byte)0xcb,(byte)0x2d,(by te)0x91,(byte)0x48,(byte)0x17,(byte)0xdf,(byte)0xb3,(byte)0x01,(byte)0x63,(byte) 0xc5,(byte)0x8f,(byte)0xbe,(byte)0xc0,(byte)0x57,(byte)0xac,(byte)0x2d,(byte)0x6 1,(byte)0xee,(byte)0xbc,(byte)0x6b,(byte)0xc9,(byte)0x21,(byte)0x14,(byte)0xea,( byte)0x3a,(byte)0x4e,(byte)0x93,(byte)0x67,

------ conversions fingerprint ----------

05145790293a242735c102b13bca5821fcc0332b41ee17b454dcc5ff3c4e0eb561

=================== atalk-android.apk ==================

An unofficial release of the atalk-android is available for anyone who like to try. It can be downloaded from the link below.

Please note this is one off debug version release for anyone who want to try. Some of the debug tools are only available on debug version.

http://atalk.sytes.net/releases/atalk-android/aTalk-debug_V8.1.0.apk

Case #1. open main menu and select

Settings… | Chat Security | Delete OMEMO identities

Case # 4. Open main menu and select

Account settings… | Refresh Persistent Store (icon swipe) | check XEP-0384: OMEMO Encryption ==> Refresh

Need to exit and relaunch atalk-android.

Note: This action in case #4 is needed before upgrade to the next release for SQLite BackEndData support for OMEMO. Otherwise the OMEMO_Store is left un-touch.

// ============ atalk log ======================

07-01 02:56:59.076 D/SMACK: SENT (0):

07-01 02:56:59.086 I/αTalk: [9] impl.msghistory.MessageHistoryServiceImpl.findRecentMessagesPerContact().621 Find recent message for: Jabber:leopard@atalk.org -> abc123@icrypto.com

07-01 02:56:59.116 I/αTalk: [9] impl.msghistory.MessageHistoryServiceImpl.findRecentMessagesPerContact().621 Find recent message for: Jabber:leopard@atalk.org -> hawk@atalk.org

07-01 02:56:59.256 D/SMACK: RECV (0):

07-01 02:56:59.256 D/SMACK: SENT (0):

07-01 02:56:59.266 D/SMACK: RECV (0): 01creation@001498:848608:676287creation@001498:848608:676 287

07-01 02:56:59.456 D/SMACK: RECV (0):

07-01 02:56:59.466 D/SMACK: SENT (0):

07-01 02:56:59.606 E/αTalk: [4] org.jivesoftware.smack.AbstractXMPPConnection.callConnectionAuthenticatedListen er() Exception in authenticated listener

java.lang.NullPointerException: Attempt to invoke virtual method ‘org.whispersystems.libsignal.ecc.ECKeyPair org.whispersystems.libsignal.state.SignedPreKeyRecord.getKeyPair()’ on a null object reference

at org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil.signedPreKeyPublicForBu ndle(SignalOmemoKeyUtil.java:207)

at org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil.signedPreKeyPublicForBu ndle(SignalOmemoKeyUtil.java:56)

at org.jivesoftware.smackx.omemo.OmemoStore.packOmemoBundle(OmemoStore.java:209)

at org.jivesoftware.smackx.omemo.OmemoService.publishBundle(OmemoService.java:301)

at org.jivesoftware.smackx.omemo.OmemoService.initialize(OmemoService.java:228)

at org.jivesoftware.smackx.omemo.OmemoManager.initialize(OmemoManager.java:189)

at org.jivesoftware.smackx.omemo.OmemoManager$1.authenticated(OmemoManager.java:65 8)

at org.jivesoftware.smack.AbstractXMPPConnection.callConnectionAuthenticatedListen er(AbstractXMPPConnection.java:1262)

at org.jivesoftware.smack.AbstractXMPPConnection.afterSuccessfulLogin(AbstractXMPP Connection.java:574)

at org.jivesoftware.smack.tcp.XMPPTCPConnection.afterSuccessfulLogin(XMPPTCPConnec tion.java:378)

at org.jivesoftware.smack.tcp.XMPPTCPConnection.loginInternal(XMPPTCPConnection.ja va:443)

at org.jivesoftware.smack.AbstractXMPPConnection.login(AbstractXMPPConnection.java :493)

at net.java.sip.communicator.impl.protocol.jabber.LoginByPasswordStrategy.login(Lo ginByPasswordStrategy.java:114)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.connectAndLogin(ProtocolProviderServiceJabberImpl.java:1145)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.connectAndLogin(ProtocolProviderServiceJabberImpl.java:901)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.initializeConnectAndLogin(ProtocolProviderServiceJabberImpl.java:749)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.register(ProtocolProviderServiceJabberImpl.java:526)

at net.java.sip.communicator.util.account.LoginManager$RegisterProvider.run(LoginM anager.java:325)

Hi!

#1: I think it may be good to unpublish the old deviceId in the future, although I don’t think it is absolutely necessary. Thank you for the hint anyways

#2: Currently regenerate() deletes the whole (file based) store, so all contacts keys and sessions are lost. This list is repopulated after creating new sessions/getting deviceList updates again. Originally I planned to only delete sessions and keep contacts keys and trust, but due to a limitation of the file based store I went with deleting everything for now. You should be able to implement your own behaviour in your SQL store implementation though.

#3: This is probably caused by the deviceId issue you reported earlier. I’ll hopefully push a fix for this soon, but the problem is not trivial unfortunately :confused:

#4: Probably also caused by the defaultDeviceId bug.

#5: Conversations uses an older version of the library and the fingerprint method changed. I suggest to use the getOmemoFingerprint methods to get fingerprints compatible with Conversations.

Once again thank you very much for your report! I’ll try my best to fix the issues soon.

Thanks for the input:

#5: Conversations uses an older version of the library and the fingerprint method changed. I suggest to use the getOmemoFingerprint methods to get fingerprints compatible with Conversations.

Found that aTalk must store conversions fingerprint style in SQLite database. Otherwise it will not work for isTrustedOmemoIdentity() etc that uses OmemoFingerprint as parameter.

I cannot found the mentioned getOmemoFingerprint, so aTalk extracts the

String fingerprint = getFingerprint(omemoManager).toString();

and pass fingerprint string to database for storage.

There are mOmemoManager.getOurFingerprint(); as well as mOmemoManager.getFingerprint(omemoDevice) to get Fingerprints from known devices. If you want to calculate the fingerprint of a signal IdentityKey directl (even though this is probably not needed), you can use SignalOmemoKeyUtil.getFingerprint(IdentityKey).

Those methods return a OmemoFingerprint object, which wrapps a String. You can get the underlying Fingerprint-String using omemoFingerprint.toString().

Thanks. aTalk implemented the methods as recommended; and it has the following method for storing the IdentityKeyPair in DatabaseBackend.

However when it executes the statement:

String fingerprint = omemoManager.getOurFingerprint().toString();

omemoManager instead of using its existing identityKeyPair, it tries to retrieve the identityKeyPair from SQLiteOmemoStore before it is being stored using;

loadOmemoIdentityKeyPair(omemoManager)

hence returns null and throws an exception as shown in log below.

using the previous method below also having the same problem:

String fingerprint = getFingerprint(omemoManager).toString();

============ SQLiteOmemoStore class ===========

@Override
public void storeOmemoIdentityKeyPair(OmemoManager omemoManager,
IdentityKeyPair identityKeyPair)
{
mDB.storeIdentityKeyPair(omemoManager, identityKeyPair);
}

============ BackendDatabase class ===============

public void storeIdentityKeyPair(OmemoManager omemoManager, IdentityKeyPair identityKeyPair)
{
OmemoDevice device = omemoManager.getOwnDevice();
String fingerprint = omemoManager.getOurFingerprint().toString();

storeIdentityKey(device, fingerprint,
Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT),
FingerprintStatus.createActiveVerified(false));
}

========= aTalk log Exception ===============

07-03 10:20:23.088 E/αTalk: [5] org.jivesoftware.smack.AbstractXMPPConnection.callConnectionAuthenticatedListen er() Exception in authenticated listener

java.lang.NullPointerException: Attempt to invoke virtual method ‘org.whispersystems.libsignal.IdentityKey org.whispersystems.libsignal.IdentityKeyPair.getPublicKey()’ on a null object reference

at org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil.identityKeyFromPair(Sig nalOmemoKeyUtil.java:182)

at org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil.identityKeyFromPair(Sig nalOmemoKeyUtil.java:56)

at org.jivesoftware.smackx.omemo.OmemoStore.getFingerprint(OmemoStore.java:742)

at org.jivesoftware.smackx.omemo.OmemoManager.getOurFingerprint(OmemoManager.java: 529)

at org.atalk.persistance.DatabaseBackend.storeIdentityKeyPair(DatabaseBackend.java :1177)

at org.atalk.crypto.omemo.SQLiteOmemoStore.storeOmemoIdentityKeyPair(SQLiteOmemoSt ore.java:573)

at org.atalk.crypto.omemo.SQLiteOmemoStore.storeOmemoIdentityKeyPair(SQLiteOmemoSt ore.java:37)

at org.jivesoftware.smackx.omemo.OmemoStore.regenerate(OmemoStore.java:107)

at org.jivesoftware.smackx.omemo.OmemoService.regenerate(OmemoService.java:271)

at org.jivesoftware.smackx.omemo.OmemoService.initialize(OmemoService.java:220)

at org.jivesoftware.smackx.omemo.OmemoManager.initialize(OmemoManager.java:189)

at org.jivesoftware.smackx.omemo.OmemoManager$1.authenticated(OmemoManager.java:65 8)

at org.jivesoftware.smack.AbstractXMPPConnection.callConnectionAuthenticatedListen er(AbstractXMPPConnection.java:1262)

at org.jivesoftware.smack.AbstractXMPPConnection.afterSuccessfulLogin(AbstractXMPP Connection.java:574)

at org.jivesoftware.smack.tcp.XMPPTCPConnection.afterSuccessfulLogin(XMPPTCPConnec tion.java:378)

at org.jivesoftware.smack.tcp.XMPPTCPConnection.loginInternal(XMPPTCPConnection.ja va:443)

at org.jivesoftware.smack.AbstractXMPPConnection.login(AbstractXMPPConnection.java :493)

at net.java.sip.communicator.impl.protocol.jabber.LoginByPasswordStrategy.login(Lo ginByPasswordStrategy.java:114)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.connectAndLogin(ProtocolProviderServiceJabberImpl.java:1145)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.connectAndLogin(ProtocolProviderServiceJabberImpl.java:901)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.initializeConnectAndLogin(ProtocolProviderServiceJabberImpl.java:749)

at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImp l.register(ProtocolProviderServiceJabberImpl.java:526)

at net.java.sip.communicator.util.account.LoginManager$RegisterProvider.run(LoginM anager.java:325)

07-03 10:20:29.288 D/SMACK: XMPPConnection authenticated (XMPPTCPConnection[leopard@icrypto.com/atalk] (1))

To fix the problem, aTalk uses the solution below and just tested working:

String fingerprint = getFingerprint(identityKeyPair.getPublicKey());

public String getFingerprint(IdentityKey identityKey) {
String fp = identityKey.getFingerprint();
// Cut “(byte)0x” prefixes, remove spaces and commas, cut first two digits.
fp = fp.replace("(byte)0x", “”).replace(",", “”).replace(" ", “”).substring(2);
return fp;
}

Could you note, which methods you implemented and which are methods from Smacks implementation? Otherwise it is getting a little confusing.

============ BackendDatabase class ===============

public void storeIdentityKeyPair(OmemoManager omemoManager, IdentityKeyPair identityKeyPair)
{
OmemoDevice device = omemoManager.getOwnDevice();
String fingerprint = omemoManager.getOurFingerprint().toString();

storeIdentityKey(device, fingerprint,
Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT),
FingerprintStatus.createActiveVerified(false));
}

This cannot work due to the call to getOurFingerprint(), whic tries to load the users IdentityKeyPair which is not yet available at that point in time. I’d suggest to use SignalOmemoKeyUtil.getFingerprint(IdentityKey) to get the fingerprint of a OMEMO IdentityKey instead of writing the code yourselves.

Btw: I’m not sure, what you are trying to do in the storeIdentityKeyPair method anyways. Why do you need the fingerprint?

I see that you do some kind of trust management there, which is not needed. A users IdentityKeyPair is trusted by default.

I’d suggest to separate between the users IdentityKeyPair and contacts identityKeys.

The users keyPair should not be stored along contacts identityKeys, so you should keep them appart.

I’d recommend implementing the SignalOmemoStore’s methods as if they were getters and setters.

storeOmemoIdentityKey() should put the key into the database and loadOmemoIdentityKey() should retrieve it. There should be no other logic inside those methods, since smack-omemo is handling the rest for you.

Thanks for the inputs and it really helps in aTalk omemo implementation.

I am learning OMEMO and its implementation while incorporating the feature for aTalk. I make certain assumptions at the time of implementing this feature, which may not necessary be correct at the later stage. Following is my current implementation and assumptions.

aTalk implements BlindTrustBeforeVerification option for user selection. An identityKey trust status is decided based on this option when a new identityKey is being stored; see below override method SQLiteOmemoStore#storeOmemoIdentityKey(); The public clase SQLiteOmemoStore extends SignalOmemoStore. This is the reason why the fingerprint for the key is retrieved and stored alongside the identity during the storeOmemoIdentityKey() process.

I was thinking what happen if user decide not to trust the key at a later stage; hence need to implement an UI to display all the stored fingerprints and their trust status retrieved from SQL database, and user can then modify the trust state of each fingerprint via this UI (yet to implement). Storing all the omemo fingerprints and their trust state in database makes this implementation easier; instead of getfingerprint() for all the identities when user access to this UI. Actually storing the fingerprint in database also helps me in learning and debug during omemo implementation. aTalk has an built-in tool to retrieve all the aTalk stored information, and view the sqlite database tables structure and their contents on a PC.

I initial tried to use SignalOmemoKeyUtil.getFingerprint(IdentityKey), but need an instance of SignalOmemoKeyUtil since the method getFingerprint(IdentityKey) is not static; However I was unable to find a way to get this instance, so I proceed to implement the method “String getFingerprint(IdentityKey identityKey)” for aTalk.

FYI: aTalk SQLiteOmemoStore implementation for omemo chat support is now fully working with multiple accounts. The initial aTalk omemo database structure needs to be changed multiple times, due to incorrect assumption made earlier, in order to make it works.

/**

  • Store the public identityKey of the device. If new, initialize its fingerprint trust status

  • pending user option BlindTrustBeforeVerification. Else set its status to active and update

  • lastActivation to current.

  • @param omemoManager

  • omemoManager of our device.

  • @param device

  • The remote client’s device

  • @param identityKey

  • The remote client’s identity key.
    */
    @Override
    public void storeOmemoIdentityKey(OmemoManager omemoManager, OmemoDevice device,
    IdentityKey identityKey)
    {
    String name = device.getJid().toString();
    String fingerprint = null;
    fingerprint = mDB.getFingerprint(identityKey);

    if (!mDB.loadIdentityKeys(device).contains(identityKey)) {
    FingerprintStatus status = getFingerprintStatus(device, fingerprint);
    if (status == null) {
    ConfigurationService mConfig = AndroidGUIActivator.getConfigurationService();
    if (mConfig.isBlindTrustBeforeVerification()) {
    logger.info("Blind trusted fingerprint for: " + device.getJid());
    status = FingerprintStatus.createActiveTrusted();
    }
    else {
    status = FingerprintStatus.createActiveUndecided();
    }
    }
    else {
    status = status.toActive();
    }
    mDB.storeIdentityKey(device, identityKey, status);
    trustCache.remove(fingerprint);
    }
    }

Thanks for the inputs and it really helps in aTalk omemo implementation.

I am learning OMEMO and its implementation while incorporating the feature for aTalk. I make certain assumptions at the time of implementing this feature, which may not necessary be correct at the later stage. Following is my current implementation and assumptions.

I fully understand . I’m also quite new to XMPP, so I can relate .

aTalk implements BlindTrustBeforeVerification option for user selection. An identityKey trust status is decided based on this option when a new identityKey is being stored; see below override method SQLiteOmemoStore#storeOmemoIdentityKey(); The public clase SQLiteOmemoStore extends SignalOmemoStore. This is the reason why the fingerprint for the key is retrieved and stored alongside the identity during the storeOmemoIdentityKey() process.
Daniel Gultsch wrote a nice article about BTBV. Basically BTBV works as follows:

When a new key k is received for a Jid J, then k is only considered automatically trusted, when there is no other key n of J, which has been manually trusted (verified). As soon as there is such a key, k will be considered undecided. So a new key does only get considered blindly trusted, when no other key has been manually trusted.

You’d have to query all keys of J in order to look for manually trusted keys.

Currently smack-omemo only knows the states trusted, untrusted and undecided. It might be worth considering adding more states (blindTrusted, verified) to allow easier implementation of BTBV in the future. It should probably be possible to implement it without such changes though.

I initial tried to use SignalOmemoKeyUtil.getFingerprint(IdentityKey), but need an instance of SignalOmemoKeyUtil since the method getFingerprint(IdentityKey) is not static; However I was unable to find a way to get this instance, so I proceed to implement the method “String getFingerprint(IdentityKey identityKey)” for aTalk.

You can do mOmemoStore.keyUtil() to get the Singleton instance of the keyUtil used throughout smack-omemo .

You’d have to query all keys of J in order to look for manually trusted keys.

Thanks. I review the source and its implementation based on your input; it does fulfill the specified requirements. Actually I have adopted the FingerprintStatus class implementation from conversations for aTalk. FingerprintStatus has the following defined Trust states; and null == not manually trusted.

public enum Trust {
COMPROMISED,
UNDECIDED,
UNTRUSTED,
TRUSTED,
VERIFIED,
VERIFIED_X509
}

You can do mOmemoStore.keyUtil() to get the Singleton instance of the keyUtil used throughout smack-omemo .
aTalk has changed to your recommendation.
FingerprintStatus.java.zip (1605 Bytes)
SQLiteOmemoStore.java.zip (6071 Bytes)

In the course of Omemo regenerate() or purgeDevices() processes, do they also take care to clean up any old published data on the XMPP server for the deleted omemoDevices?

Is so, what is the method? Can aTalk accesses to this method for its own feature implementation.

During aTalk OMEMO testing, I found that there are few old omemoDevice’s stored on the server database, possibly left behind when Omemo tables are being created or modified without properly cleanup. This seems to have effect on both the aTalk and conversation client to properly setup the omemo session for omemo communications.

smack-omemo does not clean up any traces left on the server. The OMEMO protocol currently hasn’t specified any cleanup stuff. But I might discuss this with the other OMEMO devs.

cmeng wrote:

During aTalk OMEMO testing, I found that there are few old omemoDevice’s stored on the server database, possibly left behind when Omemo tables are being created or modified without properly cleanup. This seems to have effect on both the aTalk and conversation client to properly setup the omemo session for omemo communications.

How does this have an effect on session creation? The device list is cleared when purgeDevices() is called and when a new session is created, only those bundles with IDs in the device list are fetched, so I don’t see, how old bundles could affect session creation.

In my testing, I did a recreate new for all the omemo sql tables (without using the regenerate). After the tables creation, I cannot use regenerate() as it needs the old deviceId. So I restart the aTalk application to let omemoManager to create the new omemo table data.

After that aTalk seems unable to setup a proper omemo chat with conversations with multiple attempts, randomly falling back to only one way traffic. I see conversations did trying to fetch the new keys but unsuccessful; complaining key mismatch, key fetch failed etc. Eventually I did a manual purging of all the pubsub items for atalk accounts on the xmpp ejabberd server. Restart both aTalk and conversations clients, only then omemo chat can proceed without problem.

I did not go into detail investigating why conversations is unable to fetch new keys; but seems that the omemo encrypted messages are being exchanged between the two clients. I was wondering why conversations tries unsuccessful to fetch the new keys published by aTalk after seeing the new encrypted message with the not matching its stored buddy. This leads me to think the cause may due to old device records left on the server, and conversations is always fetching the old record instead of newly published record by aTalk.

OR xmpp server always returns the first found record in table which is likely to be the old record when requested

OR current protocol just does not provide the facility to allow the client to fetch all the device records for the specified buddy; the client can then select the matching device ID with the received message to use.