diff --git a/data/src/main/java/org/mercury_im/messenger/data/converter/Base64PGPPublicKeyRingConverter.java b/data/src/main/java/org/mercury_im/messenger/data/converter/Base64PGPPublicKeyRingConverter.java new file mode 100644 index 0000000..8e43f7b --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/converter/Base64PGPPublicKeyRingConverter.java @@ -0,0 +1,48 @@ +package org.mercury_im.messenger.data.converter; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.util.encoders.Base64; +import org.pgpainless.PGPainless; + +import java.nio.charset.StandardCharsets; + +import javax.annotation.Nullable; + +import io.requery.Converter; +import lombok.SneakyThrows; + +public class Base64PGPPublicKeyRingConverter implements Converter { + @Override + public Class getMappedType() { + return PGPPublicKeyRing.class; + } + + @Override + public Class getPersistedType() { + return String.class; + } + + @Nullable + @Override + public Integer getPersistedSize() { + return null; + } + + @SneakyThrows + @Override + public String convertToPersisted(PGPPublicKeyRing value) { + if (value == null) { + return null; + } + return new String(Base64.encode(value.getEncoded()), StandardCharsets.UTF_8); + } + + @SneakyThrows + @Override + public PGPPublicKeyRing convertToMapped(Class type, @Nullable String value) { + if (value == null) { + return null; + } + return PGPainless.readKeyRing().publicKeyRing(Base64.decode(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/converter/Base64PGPSecretKeyRingConverter.java b/data/src/main/java/org/mercury_im/messenger/data/converter/Base64PGPSecretKeyRingConverter.java new file mode 100644 index 0000000..4a965be --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/converter/Base64PGPSecretKeyRingConverter.java @@ -0,0 +1,48 @@ +package org.mercury_im.messenger.data.converter; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Base64; +import org.pgpainless.PGPainless; + +import java.nio.charset.StandardCharsets; + +import javax.annotation.Nullable; + +import io.requery.Converter; +import lombok.SneakyThrows; + +public class Base64PGPSecretKeyRingConverter implements Converter { + @Override + public Class getMappedType() { + return PGPSecretKeyRing.class; + } + + @Override + public Class getPersistedType() { + return String.class; + } + + @Nullable + @Override + public Integer getPersistedSize() { + return null; + } + + @SneakyThrows + @Override + public String convertToPersisted(PGPSecretKeyRing value) { + if (value == null) { + return null; + } + return new String(Base64.encode(value.getEncoded()), StandardCharsets.UTF_8); + } + + @SneakyThrows + @Override + public PGPSecretKeyRing convertToMapped(Class type, @Nullable String value) { + if (value == null) { + return null; + } + return PGPainless.readKeyRing().secretKeyRing(Base64.decode(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/converter/OpenPGPSecretKeyBackupPassphraseConverter.java b/data/src/main/java/org/mercury_im/messenger/data/converter/OpenPGPSecretKeyBackupPassphraseConverter.java new file mode 100644 index 0000000..c7d4184 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/converter/OpenPGPSecretKeyBackupPassphraseConverter.java @@ -0,0 +1,36 @@ +package org.mercury_im.messenger.data.converter; + +import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase; + +import javax.annotation.Nullable; + +import io.requery.Converter; + +public class OpenPGPSecretKeyBackupPassphraseConverter + implements Converter { + @Override + public Class getMappedType() { + return OpenPgpSecretKeyBackupPassphrase.class; + } + + @Override + public Class getPersistedType() { + return String.class; + } + + @Nullable + @Override + public Integer getPersistedSize() { + return null; + } + + @Override + public String convertToPersisted(OpenPgpSecretKeyBackupPassphrase value) { + return value == null ? null : value.toString(); + } + + @Override + public OpenPgpSecretKeyBackupPassphrase convertToMapped(Class type, @Nullable String value) { + return value == null ? null : new OpenPgpSecretKeyBackupPassphrase(value); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java b/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java index a657f8c..9554170 100644 --- a/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java +++ b/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java @@ -1,9 +1,11 @@ package org.mercury_im.messenger.data.di; +import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository; import org.mercury_im.messenger.core.data.repository.AccountRepository; import org.mercury_im.messenger.core.data.repository.DirectChatRepository; import org.mercury_im.messenger.core.data.repository.EntityCapsRepository; import org.mercury_im.messenger.core.data.repository.GroupChatRepository; +import org.mercury_im.messenger.core.data.repository.IkeyRecordRepository; import org.mercury_im.messenger.core.data.repository.MessageRepository; import org.mercury_im.messenger.core.data.repository.OpenPgpRepository; import org.mercury_im.messenger.core.data.repository.PeerRepository; @@ -17,6 +19,7 @@ import org.mercury_im.messenger.data.repository.RxAccountRepository; import org.mercury_im.messenger.data.repository.RxDirectChatRepository; import org.mercury_im.messenger.data.repository.RxEntityCapsRepository; import org.mercury_im.messenger.data.repository.RxGroupChatRepository; +import org.mercury_im.messenger.data.repository.RxIkeyRepository; import org.mercury_im.messenger.data.repository.RxMessageRepository; import org.mercury_im.messenger.data.repository.RxOpenPgpRepository; import org.mercury_im.messenger.data.repository.RxPeerRepository; @@ -91,4 +94,10 @@ public class RepositoryModule { ReactiveEntityStore data, AccountRepository accountRepository) { return new RxOpenPgpRepository(data, accountRepository); } + + @Provides + @Singleton + static IkeyRepository provideIkeyRepository(ReactiveEntityStore data) { + return new RxIkeyRepository(data); + } } diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeyRecordModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeyRecordModel.java new file mode 100644 index 0000000..fa7c004 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeyRecordModel.java @@ -0,0 +1,62 @@ +package org.mercury_im.messenger.data.model; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.jxmpp.jid.EntityBareJid; +import org.mercury_im.messenger.data.converter.EntityBareJidConverter; +import org.mercury_im.messenger.data.converter.OpenPgpV4FingerprintConverter; +import org.pgpainless.key.OpenPgpV4Fingerprint; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import io.requery.CascadeAction; +import io.requery.Column; +import io.requery.Convert; +import io.requery.Entity; +import io.requery.Generated; +import io.requery.Index; +import io.requery.Key; +import io.requery.OneToMany; +import io.requery.Table; +import io.requery.converter.UUIDConverter; + +@Table(name = "ikey_record", uniqueIndexes = {"record_index"}) +@Entity +public class AbstractIkeyRecordModel { + + @Key + @Generated + UUID id; + + @Column(name = "account", unique = true) + @Index("record_index") + @Convert(UUIDConverter.class) + UUID accountId; + + @Column(name = "jid", unique = true) + @Index("record_index") + @Convert(EntityBareJidConverter.class) + EntityBareJid jid; + + @Column(name = "\"timestamp\"") + Date timestamp; + + @OneToMany(mappedBy = "record", cascade = {CascadeAction.DELETE, CascadeAction.SAVE}) + @Column + List subordinates; + + @Column(name = "superordinate") + PGPPublicKeyRing superordinate; + + @Column(name = "fingerprint") + @Convert(OpenPgpV4FingerprintConverter.class) + OpenPgpV4Fingerprint fingerprint; + + @Column(name = "trusted") + boolean trusted; + + @Column(name = "proof") + String proof; + +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeySecretKeyModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeySecretKeyModel.java new file mode 100644 index 0000000..39cbd90 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeySecretKeyModel.java @@ -0,0 +1,40 @@ +package org.mercury_im.messenger.data.model; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase; +import org.mercury_im.messenger.data.converter.Base64PGPSecretKeyRingConverter; +import org.mercury_im.messenger.data.converter.OpenPGPSecretKeyBackupPassphraseConverter; +import org.pgpainless.key.OpenPgpV4Fingerprint; + +import java.util.UUID; + +import io.requery.Column; +import io.requery.Convert; +import io.requery.Entity; +import io.requery.Key; +import io.requery.Table; +import io.requery.converter.UUIDConverter; + +@Table(name = "ikey_sec_key") +@Entity +public class AbstractIkeySecretKeyModel { + + @Key + @Convert(UUIDConverter.class) + @Column(name = "account") + UUID accountId; + + @Column(name = "fingerprint") + OpenPgpV4Fingerprint fingerprint; + + @Column(name = "backup_passphrase") + @Convert(OpenPGPSecretKeyBackupPassphraseConverter.class) + OpenPgpSecretKeyBackupPassphrase backupPassphrase; + + @Column(name = "key") + @Convert(Base64PGPSecretKeyRingConverter.class) + PGPSecretKeyRing key; + + @Column(name = "trusted") + boolean trusted; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeySubordinateModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeySubordinateModel.java new file mode 100644 index 0000000..d7b411a --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractIkeySubordinateModel.java @@ -0,0 +1,35 @@ +package org.mercury_im.messenger.data.model; + +import java.net.URI; +import java.util.UUID; + +import io.requery.Column; +import io.requery.Convert; +import io.requery.Entity; +import io.requery.Generated; +import io.requery.Key; +import io.requery.ManyToOne; +import io.requery.Table; +import io.requery.converter.URIConverter; + +@Table(name = "ikey_subordinates") +@Entity +public class AbstractIkeySubordinateModel { + + @Key + @Generated + UUID id; + + @Column(name = "type") + String type; + + @Column(name = "uri") + @Convert(URIConverter.class) + URI uri; + + @Column(name = "fpr") + String fpr; + + @ManyToOne + AbstractIkeyRecordModel record; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/RxIkeyRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/RxIkeyRepository.java new file mode 100644 index 0000000..8435ae1 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/RxIkeyRepository.java @@ -0,0 +1,146 @@ +package org.mercury_im.messenger.data.repository; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.jivesoftware.smackx.ikey.element.IkeyElement; +import org.jivesoftware.smackx.ikey.element.ProofElement; +import org.jivesoftware.smackx.ikey.element.SignedElement; +import org.jivesoftware.smackx.ikey.element.SubordinateElement; +import org.jivesoftware.smackx.ikey.element.SubordinateListElement; +import org.jivesoftware.smackx.ikey.element.SuperordinateElement; +import org.jivesoftware.smackx.ikey.mechanism.IkeyType; +import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase; +import org.jxmpp.jid.EntityBareJid; +import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository; +import org.mercury_im.messenger.data.model.IkeyRecordModel; +import org.mercury_im.messenger.data.model.IkeySecretKeyModel; +import org.mercury_im.messenger.data.model.IkeySubordinateModel; +import org.pgpainless.PGPainless; +import org.pgpainless.key.OpenPgpV4Fingerprint; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; + +public class RxIkeyRepository implements IkeyRepository { + + private final ReactiveEntityStore data; + + public RxIkeyRepository(ReactiveEntityStore data) { + this.data = data; + } + + @Override + public Maybe loadSecretKey(UUID accountId) { + return data.select(IkeySecretKeyModel.class) + .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId)) + .get() + .maybe() + .map(IkeySecretKeyModel::getKey); + } + + @Override + public Completable storeSecretKey(UUID accountId, PGPSecretKeyRing secretKey) { + + return data.select(IkeySecretKeyModel.class) + .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId)) + .get().observable() + .single(new IkeySecretKeyModel()) + .map(m -> { + m.setAccountId(accountId); + m.setKey(secretKey); + m.setFingerprint(new OpenPgpV4Fingerprint(secretKey.getPublicKey())); + m.setBackupPassphrase(m.getBackupPassphrase()); + m.setTrusted(m.isTrusted()); + return m; + }) + .flatMap(data::upsert) + .ignoreElement(); + } + + @Override + public Single deleteSecretKey(UUID accountId) { + return data.delete().from(IkeySecretKeyModel.class) + .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId)) + .get() + .single(); + } + + @Override + public Single loadBackupPassphrase(UUID accountID) { + return data.select(IkeySecretKeyModel.class) + .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountID)) + .get() + .observable() + .singleOrError() + .map(IkeySecretKeyModel::getBackupPassphrase); + } + + @Override + public Completable storeBackupPassphrase(UUID accountId, OpenPgpSecretKeyBackupPassphrase passphrase) { + return data.select(IkeySecretKeyModel.class) + .where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId)) + .get().observable() + .single(new IkeySecretKeyModel()) + .map(m -> { + m.setAccountId(accountId); + m.setBackupPassphrase(m.getBackupPassphrase()); + return m; + }) + .flatMap(data::upsert) + .ignoreElement(); + } + + @Override + public Maybe loadRecord(UUID accountId, EntityBareJid jid) { + return data.select(IkeyRecordModel.class) + .where(IkeyRecordModel.ACCOUNT_ID.eq(accountId).and(IkeyRecordModel.JID.eq(jid))) + .get().maybe() + .map(m -> { + SuperordinateElement superordinateElement = new SuperordinateElement(m.getSuperordinate().getEncoded()); + List subList = new ArrayList<>(); + for (IkeySubordinateModel s : m.getSubordinates()) { + subList.add(new SubordinateElement(s.getType(), s.getUri(), s.getFpr())); + } + SubordinateListElement subs = new SubordinateListElement(m.getJid(), m.getTimestamp(), subList); + SignedElement signedElement = new SignedElement(subs.toBase64EncodedString()); + IkeyElement ikeyElement = new IkeyElement(IkeyType.OX, superordinateElement, signedElement, new ProofElement(m.getProof())); + return ikeyElement; + }); + } + + @Override + public Completable storeRecord(UUID accountId, EntityBareJid jid, IkeyElement record) { + assert jid.equals(record.getSignedElement().getChildElement().getJid()); + return data.select(IkeyRecordModel.class) + .where(IkeyRecordModel.ACCOUNT_ID.eq(accountId).and(IkeyRecordModel.JID.eq(jid))) + .get().observable() + .single(new IkeyRecordModel()) + .map(m -> { + m.setAccountId(accountId); + m.setJid(jid); + m.setProof(record.getProof().getBase64Signature()); + m.setFingerprint(new OpenPgpV4Fingerprint(record.getSuperordinate().getPubKeyBytes())); + m.setSuperordinate(PGPainless.readKeyRing().publicKeyRing(record.getSuperordinate().getPubKeyBytes())); + m.setTimestamp(record.getSignedElement().getChildElement().getTimestamp()); + + for (SubordinateElement s : record.getSignedElement().getChildElement().getSubordinates()) { + IkeySubordinateModel sm = new IkeySubordinateModel(); + sm.setRecord(m); + sm.setFpr(s.getFingerprint()); + sm.setType(s.getType()); + sm.setUri(s.getUri()); + m.getSubordinates().add(sm); + } + + return m; + }) + .flatMap(data::upsert) + .ignoreElement(); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java index b2b3413..543c55d 100644 --- a/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java @@ -1,13 +1,14 @@ package org.mercury_im.messenger.data.mapping; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mercury_im.messenger.data.di.component.DaggerMappingTestComponent; import org.mercury_im.messenger.data.model.AccountModel; import org.mercury_im.messenger.entity.Account; import javax.inject.Inject; -import static junit.framework.TestCase.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class AccountMappingTest { diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java index a84dee5..6df07ce 100644 --- a/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java @@ -1,13 +1,13 @@ package org.mercury_im.messenger.data.mapping; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mercury_im.messenger.data.di.component.DaggerMappingTestComponent; import org.mercury_im.messenger.data.model.EntityCapsModel; import org.mercury_im.messenger.entity.caps.EntityCapsRecord; import javax.inject.Inject; -import static junit.framework.TestCase.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class EntityCapsMappingTest { diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/OpenPgpKeyConverterTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/OpenPgpKeyConverterTest.java new file mode 100644 index 0000000..02ba16b --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/OpenPgpKeyConverterTest.java @@ -0,0 +1,52 @@ +package org.mercury_im.messenger.data.mapping; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mercury_im.messenger.data.converter.Base64PGPPublicKeyRingConverter; +import org.mercury_im.messenger.data.converter.Base64PGPSecretKeyRingConverter; +import org.pgpainless.PGPainless; +import org.pgpainless.key.collection.PGPKeyRing; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OpenPgpKeyConverterTest { + + private static PGPKeyRing keyRing; + + @BeforeAll + public static void before() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + keyRing = PGPainless.generateKeyRing().simpleEcKeyRing("xmpp:alice@wonderland.lit"); + } + + @Test + public void testPublicKeyConverter() throws IOException { + Base64PGPPublicKeyRingConverter converter = new Base64PGPPublicKeyRingConverter(); + String expected = Base64.toBase64String(keyRing.getPublicKeys().getEncoded()); + + String base64 = converter.convertToPersisted(keyRing.getPublicKeys()); + assertEquals(expected, base64); + + PGPPublicKeyRing pub = converter.convertToMapped(PGPPublicKeyRing.class, base64); + assertEquals(keyRing.getPublicKeys().getPublicKey().getKeyID(), pub.getPublicKey().getKeyID()); + } + + @Test + public void testSecretKeyConverter() throws IOException { + Base64PGPSecretKeyRingConverter converter = new Base64PGPSecretKeyRingConverter(); + String expected = Base64.toBase64String(keyRing.getSecretKeys().getEncoded()); + + String base64 = converter.convertToPersisted(keyRing.getSecretKeys()); + assertEquals(expected, base64); + + PGPSecretKeyRing sec = converter.convertToMapped(PGPSecretKeyRing.class, base64); + assertEquals(keyRing.getSecretKeys().getPublicKey().getKeyID(), sec.getPublicKey().getKeyID()); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java index c5ed545..dcd7440 100644 --- a/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java @@ -1,6 +1,6 @@ package org.mercury_im.messenger.data.mapping; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mercury_im.messenger.data.di.component.DaggerMappingTestComponent; import org.mercury_im.messenger.data.model.AccountModel; import org.mercury_im.messenger.data.model.PeerModel; @@ -9,7 +9,7 @@ import org.mercury_im.messenger.entity.contact.SubscriptionDirection; import javax.inject.Inject; -import static junit.framework.TestCase.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class PeerMappingTest { diff --git a/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java b/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java index 6b8aad6..cb066ff 100644 --- a/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java +++ b/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java @@ -1,6 +1,6 @@ package org.mercury_im.messenger.data.repository; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mercury_im.messenger.data.di.component.DaggerInMemoryDatabaseComponent; import org.mercury_im.messenger.data.di.component.InMemoryDatabaseComponent; import org.mercury_im.messenger.entity.Account; @@ -16,6 +16,8 @@ import io.reactivex.disposables.CompositeDisposable; import io.requery.Persistable; import io.requery.reactivex.ReactiveEntityStore; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class AccountRepositoryTest { private static final Logger LOGGER = Logger.getLogger(AccountRepositoryTest.class.getName()); @@ -119,13 +121,14 @@ public class AccountRepositoryTest { d.dispose(); } - @Test(expected = NoSuchElementException.class) + @Test public void updateMissingEntityFails() { Account missingAccount = new Account(); missingAccount.setAddress("this@account.is.missing"); missingAccount.setPassword("inTheDatabase"); - accountRepository.updateAccount(missingAccount) - .blockingGet(); + assertThrows(NoSuchElementException.class, () -> + accountRepository.updateAccount(missingAccount) + .blockingGet()); } } diff --git a/domain/src/main/java/org/jivesoftware/smackx/ikey/IkeyManager.java b/domain/src/main/java/org/jivesoftware/smackx/ikey/IkeyManager.java index bd56deb..b83e435 100644 --- a/domain/src/main/java/org/jivesoftware/smackx/ikey/IkeyManager.java +++ b/domain/src/main/java/org/jivesoftware/smackx/ikey/IkeyManager.java @@ -65,12 +65,16 @@ public final class IkeyManager extends Manager { public void startListeners() { PepManager.getInstanceFor(connection()) - .addPepEventListener(IkeyConstants.SUBORDINATES_NODE, IkeyElement.class, pepEventListener); + .addPepEventListener(IkeyConstants.SUBORDINATES_NODE, IkeyElement.class, ikeyPepEventListener); } public void stopListeners() { PepManager.getInstanceFor(connection()) - .removePepEventListener(pepEventListener); + .removePepEventListener(ikeyPepEventListener); + } + + public void setStore(IkeyStore store) { + this.store = store; } public IkeyElement createIkeyElement(IkeySignatureCreationMechanism mechanism, @@ -136,6 +140,15 @@ public final class IkeyManager extends Manager { store.storeIkeyRecord(from, element); } + public IkeyElement getIkeyElementOf(EntityBareJid from) throws IOException, InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException { + IkeyElement stored = store.loadIkeyRecord(from); + if (stored == null) { + stored = fetchIkeyElementOf(from); + store.storeIkeyRecord(from, stored); + } + return stored; + } + private boolean verifyIkeyElement(EntityBareJid from, IkeyElement element) throws IOException, UnsupportedSignatureAlgorithmException { IkeySignatureVerificationMechanism verificationMechanism = getSignatureVerificationMechanismFor(element); @@ -174,7 +187,7 @@ public final class IkeyManager extends Manager { } @SuppressWarnings("UnnecessaryAnonymousClass") - private final PepEventListener pepEventListener = new PepEventListener() { + private final PepEventListener ikeyPepEventListener = new PepEventListener() { @Override public void onPepEvent(EntityBareJid from, IkeyElement event, String id, Message carrierMessage) { try { diff --git a/domain/src/main/java/org/jivesoftware/smackx/ikey/element/SubordinateElement.java b/domain/src/main/java/org/jivesoftware/smackx/ikey/element/SubordinateElement.java index 62c824d..3a63f1b 100644 --- a/domain/src/main/java/org/jivesoftware/smackx/ikey/element/SubordinateElement.java +++ b/domain/src/main/java/org/jivesoftware/smackx/ikey/element/SubordinateElement.java @@ -13,17 +13,24 @@ import java.net.URI; public class SubordinateElement implements NamedElement { public static final String ELEMENT = "sub"; + public static final String ATTR_TYPE = "type"; public static final String ATTR_SUB_URI = "uri"; public static final String ATTR_SUB_FINGERPRINT = "fpr"; + private final String type; private final URI subUri; private final String subFingerprint; - public SubordinateElement(URI subKeyItemUri, String subKeyFingerprint) { + public SubordinateElement(String type, URI subKeyItemUri, String subKeyFingerprint) { + this.type = type; this.subUri = Objects.requireNonNull(subKeyItemUri, "uri MUST NOT be null nor empty."); this.subFingerprint = StringUtils.requireNotNullNorEmpty(subKeyFingerprint, "fpr MUST NOT be null nor empty."); } + public String getType() { + return type; + } + public URI getUri() { return subUri; } @@ -40,6 +47,7 @@ public class SubordinateElement implements NamedElement { @Override public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { return new XmlStringBuilder(this) + .attribute(ATTR_TYPE, getType()) .attribute(ATTR_SUB_URI, getUri().toString()) .attribute(ATTR_SUB_FINGERPRINT, getFingerprint()) .closeEmptyElement(); @@ -49,6 +57,7 @@ public class SubordinateElement implements NamedElement { public int hashCode() { return HashCode.builder() .append(getElementName()) + .append(getType()) .append(getFingerprint()) .append(getUri()) .build(); @@ -58,6 +67,7 @@ public class SubordinateElement implements NamedElement { public boolean equals(Object other) { return EqualsUtil.equals(this, other, (e, o) -> e .append(getElementName(), o.getElementName()) + .append(getType(), o.getType()) .append(getFingerprint(), o.getFingerprint()) .append(getUri(), o.getUri())); } diff --git a/domain/src/main/java/org/jivesoftware/smackx/ikey/provider/SubordinateListElementProvider.java b/domain/src/main/java/org/jivesoftware/smackx/ikey/provider/SubordinateListElementProvider.java index c08e53e..f4c9c45 100644 --- a/domain/src/main/java/org/jivesoftware/smackx/ikey/provider/SubordinateListElementProvider.java +++ b/domain/src/main/java/org/jivesoftware/smackx/ikey/provider/SubordinateListElementProvider.java @@ -30,10 +30,11 @@ public class SubordinateListElementProvider extends ExtensionElementProvider loadSecretKey(Account account) { + return loadSecretKey(account.getId()); + } + + Maybe loadSecretKey(UUID accountId); + + default Completable storeSecretKey(Account account, PGPSecretKeyRing secretKey) { + return storeSecretKey(account.getId(), secretKey); + } + + Completable storeSecretKey(UUID accountId, PGPSecretKeyRing secretKey); + + default Single deleteSecretKey(Account account) { + return deleteSecretKey(account.getId()); + } + + Single deleteSecretKey(UUID accountId); + + default Single loadBackupPassphrase(Account account) { + return loadBackupPassphrase(account.getId()); + } + + Single loadBackupPassphrase(UUID accountID); + + default Completable storeBackupPassphrase(Account account, OpenPgpSecretKeyBackupPassphrase passphrase) { + return storeBackupPassphrase(account.getId(), passphrase); + } + + Completable storeBackupPassphrase(UUID accountID, OpenPgpSecretKeyBackupPassphrase passphrase); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/core/data/repository/IkeyRecordRepository.java b/domain/src/main/java/org/mercury_im/messenger/core/data/repository/IkeyRecordRepository.java new file mode 100644 index 0000000..f17527b --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/core/data/repository/IkeyRecordRepository.java @@ -0,0 +1,26 @@ +package org.mercury_im.messenger.core.data.repository; + +import org.jivesoftware.smackx.ikey.element.IkeyElement; +import org.jxmpp.jid.EntityBareJid; +import org.mercury_im.messenger.entity.Account; + +import java.util.UUID; + +import io.reactivex.Completable; +import io.reactivex.Maybe; + +public interface IkeyRecordRepository { + + default Maybe loadRecord(Account account, EntityBareJid jid) { + return loadRecord(account.getId(), jid); + } + + Maybe loadRecord(UUID accountId, EntityBareJid jid); + + default Completable storeRecord(Account account, EntityBareJid jid, IkeyElement record) { + return storeRecord(account.getId(), jid, record); + } + + Completable storeRecord(UUID accountId, EntityBareJid jid, IkeyElement record); + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/core/viewmodel/ikey/IkeySecretKeyBackupCreationViewModel.java b/domain/src/main/java/org/mercury_im/messenger/core/viewmodel/ikey/IkeySecretKeyBackupCreationViewModel.java new file mode 100644 index 0000000..daa1cad --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/core/viewmodel/ikey/IkeySecretKeyBackupCreationViewModel.java @@ -0,0 +1,27 @@ +package org.mercury_im.messenger.core.viewmodel.ikey; + +import org.jivesoftware.smackx.ikey.IkeyManager; +import org.jivesoftware.smackx.ikey_ox.OxIkeyManager; +import org.mercury_im.messenger.core.connection.MercuryConnection; +import org.mercury_im.messenger.core.connection.MercuryConnectionManager; +import org.mercury_im.messenger.core.viewmodel.MercuryViewModel; +import org.mercury_im.messenger.entity.Account; + +import javax.inject.Inject; + +public class IkeySecretKeyBackupCreationViewModel implements MercuryViewModel { + + private final MercuryConnectionManager connectionManager; + + @Inject + public IkeySecretKeyBackupCreationViewModel(MercuryConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + public void createIkeySecretKeyBackup(Account account) { + MercuryConnection connection = connectionManager.getConnection(account); + IkeyManager ikeyManager = IkeyManager.getInstanceFor(connection.getConnection()); + OxIkeyManager oxIkeyManager = OxIkeyManager.getInstanceFor(connection.getConnection()); + + } +}