Wip: Ikey storage backend

This commit is contained in:
Paul Schaub 2020-10-11 11:44:47 +02:00
parent 94d213b8dd
commit 733f9684c7
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
26 changed files with 784 additions and 23 deletions

View File

@ -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<PGPPublicKeyRing, String> {
@Override
public Class<PGPPublicKeyRing> getMappedType() {
return PGPPublicKeyRing.class;
}
@Override
public Class<String> 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<? extends PGPPublicKeyRing> type, @Nullable String value) {
if (value == null) {
return null;
}
return PGPainless.readKeyRing().publicKeyRing(Base64.decode(value.getBytes(StandardCharsets.UTF_8)));
}
}

View File

@ -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<PGPSecretKeyRing, String> {
@Override
public Class<PGPSecretKeyRing> getMappedType() {
return PGPSecretKeyRing.class;
}
@Override
public Class<String> 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<? extends PGPSecretKeyRing> type, @Nullable String value) {
if (value == null) {
return null;
}
return PGPainless.readKeyRing().secretKeyRing(Base64.decode(value.getBytes(StandardCharsets.UTF_8)));
}
}

View File

@ -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<OpenPgpSecretKeyBackupPassphrase, String> {
@Override
public Class<OpenPgpSecretKeyBackupPassphrase> getMappedType() {
return OpenPgpSecretKeyBackupPassphrase.class;
}
@Override
public Class<String> 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<? extends OpenPgpSecretKeyBackupPassphrase> type, @Nullable String value) {
return value == null ? null : new OpenPgpSecretKeyBackupPassphrase(value);
}
}

View File

@ -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<Persistable> data, AccountRepository accountRepository) {
return new RxOpenPgpRepository(data, accountRepository);
}
@Provides
@Singleton
static IkeyRepository provideIkeyRepository(ReactiveEntityStore<Persistable> data) {
return new RxIkeyRepository(data);
}
}

View File

@ -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<IkeySubordinateModel> subordinates;
@Column(name = "superordinate")
PGPPublicKeyRing superordinate;
@Column(name = "fingerprint")
@Convert(OpenPgpV4FingerprintConverter.class)
OpenPgpV4Fingerprint fingerprint;
@Column(name = "trusted")
boolean trusted;
@Column(name = "proof")
String proof;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<Persistable> data;
public RxIkeyRepository(ReactiveEntityStore<Persistable> data) {
this.data = data;
}
@Override
public Maybe<PGPSecretKeyRing> 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<Integer> deleteSecretKey(UUID accountId) {
return data.delete().from(IkeySecretKeyModel.class)
.where(IkeySecretKeyModel.ACCOUNT_ID.eq(accountId))
.get()
.single();
}
@Override
public Single<OpenPgpSecretKeyBackupPassphrase> 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<IkeyElement> 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<SubordinateElement> 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();
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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<IkeyElement> pepEventListener = new PepEventListener<IkeyElement>() {
private final PepEventListener<IkeyElement> ikeyPepEventListener = new PepEventListener<IkeyElement>() {
@Override
public void onPepEvent(EntityBareJid from, IkeyElement event, String id, Message carrierMessage) {
try {

View File

@ -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()));
}

View File

@ -30,10 +30,11 @@ public class SubordinateListElementProvider extends ExtensionElementProvider<Sub
switch (parser.nextTag()) {
case START_ELEMENT:
if (SubordinateElement.ELEMENT.equals(parser.getName())) {
String type = ParserUtils.getRequiredAttribute(parser, SubordinateElement.ATTR_TYPE);
String uriString = ParserUtils.getRequiredAttribute(parser, SubordinateElement.ATTR_SUB_URI);
URI uri = URI.create(uriString);
String fingerprint = ParserUtils.getRequiredAttribute(parser, SubordinateElement.ATTR_SUB_FINGERPRINT);
subordinates.add(new SubordinateElement(uri, fingerprint));
subordinates.add(new SubordinateElement(type, uri, fingerprint));
}
break;
case END_ELEMENT:

View File

@ -18,6 +18,8 @@ import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper;
import org.jivesoftware.smackx.pep.PepManager;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.mercury_im.messenger.core.crypto.OpenPgpSecretKeyBackupPassphraseGenerator;
import org.mercury_im.messenger.core.crypto.SecureRandomSecretKeyBackupPassphraseGenerator;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import java.io.IOException;
@ -55,6 +57,18 @@ public final class OxIkeyManager extends Manager {
return OpenPgpPubSubUtil.fetchSecretKey(PepManager.getInstanceFor(connection()), SUPERORDINATE_NODE);
}
public OpenPgpSecretKeyBackupPassphrase depositSecretIdentityKey(PGPSecretKeyRing secretKey)
throws PGPException, InterruptedException, SmackException.NoResponseException,
SmackException.NotConnectedException, SmackException.FeatureNotSupportedException,
XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, IOException {
OpenPgpSecretKeyBackupPassphraseGenerator passphraseGenerator = new SecureRandomSecretKeyBackupPassphraseGenerator();
OpenPgpSecretKeyBackupPassphrase passphrase = passphraseGenerator.generateBackupPassphrase();
depositSecretIdentityKey(secretKey, passphrase);
return passphrase;
}
public void depositSecretIdentityKey(PGPSecretKeyRing secretKey, OpenPgpSecretKeyBackupPassphrase passphrase)
throws InterruptedException, SmackException.NoResponseException,
SmackException.NotConnectedException, SmackException.FeatureNotSupportedException,

View File

@ -0,0 +1,15 @@
package org.jivesoftware.smackx.ikey_ox;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
public interface OxIkeyStore {
PGPSecretKeyRing loadSecretKey();
void storeSecretKey(PGPSecretKeyRing secretKey);
OpenPgpSecretKeyBackupPassphrase loadBackupPassphrase();
void storeBackupPassphrase(OpenPgpSecretKeyBackupPassphrase passphrase);
}

View File

@ -3,6 +3,7 @@ package org.jivesoftware.smackx.ikey_utils;
import org.jivesoftware.smackx.ikey.element.SubordinateElement;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.pubsub.PubSubUri;
import org.jxmpp.jid.BareJid;
@ -18,20 +19,20 @@ public class IkeySubordinateElementCreator {
String node = OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEY(fingerprint);
PubSubUri pubSubUri = new PubSubUri(pubSubService, node, itemId, null);
URI uri = new URI(pubSubUri.toString());
return new SubordinateElement(uri, fingerprint.toString());
return new SubordinateElement(OpenPgpElement.NAMESPACE, uri, fingerprint.toString());
}
public static SubordinateElement createOmemoSubordinateElement(BareJid pubSubService, OmemoDevice device, OmemoFingerprint fingerprint)
throws URISyntaxException {
PubSubUri pubSubUri = new PubSubUri(pubSubService, "urn:xmpp:omemo:1:bundles", Integer.toString(device.getDeviceId()), null);
URI uri = new URI(pubSubUri.toString());
return new SubordinateElement(uri, fingerprint.toString());
return new SubordinateElement("urn:xmpp:omemo:1", uri, fingerprint.toString());
}
public static SubordinateElement createSiacsOmemoSubordinateElement(BareJid pubSubService, OmemoDevice device, OmemoFingerprint fingerprint)
throws URISyntaxException {
PubSubUri pubSubUri = new PubSubUri(pubSubService, device.getBundleNodeName(), null, null);
URI uri = new URI(pubSubUri.toString());
return new SubordinateElement(uri, fingerprint.toString());
return new SubordinateElement("eu.siacs.conversations.axolotl", uri, fingerprint.toString());
}
}

View File

@ -0,0 +1,8 @@
package org.mercury_im.messenger.core.crypto.ikey;
import org.mercury_im.messenger.core.data.repository.IkeyKeyRepository;
import org.mercury_im.messenger.core.data.repository.IkeyRecordRepository;
public interface IkeyRepository extends IkeyKeyRepository, IkeyRecordRepository {
}

View File

@ -1,5 +0,0 @@
package org.mercury_im.messenger.core.crypto.ikey;
public class IkeySignatureProvider {
}

View File

@ -0,0 +1,71 @@
package org.mercury_im.messenger.core.crypto.ikey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smackx.ikey.element.IkeyElement;
import org.jivesoftware.smackx.ikey.record.IkeyStore;
import org.jivesoftware.smackx.ikey_ox.OxIkeyStore;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.SchedulersFacade;
import java.io.IOException;
import java.util.UUID;
/**
* Adapter that serves as a {@link IkeyStore} and {@link OxIkeyStore} implementation
* wrapping a {@link IkeyRepository}.
*/
public class IkeyStoreAdapter implements IkeyStore, OxIkeyStore {
private final UUID accountId;
private final IkeyRepository repository;
private final SchedulersFacade schedulers;
public IkeyStoreAdapter(UUID accountId, IkeyRepository repository, SchedulersFacade schedulers) {
this.accountId = accountId;
this.repository = repository;
this.schedulers = schedulers;
}
@Override
public IkeyElement loadIkeyRecord(EntityBareJid jid) throws IOException {
return repository.loadRecord(accountId, jid)
.compose(schedulers.executeUiSafeMaybe())
.blockingGet();
}
@Override
public void storeIkeyRecord(EntityBareJid jid, IkeyElement record) throws IOException {
repository.storeRecord(accountId, jid, record)
.compose(schedulers.executeUiSafeCompletable())
.subscribe();
}
@Override
public PGPSecretKeyRing loadSecretKey() {
return repository.loadSecretKey(accountId)
.compose(schedulers.executeUiSafeMaybe())
.blockingGet();
}
@Override
public void storeSecretKey(PGPSecretKeyRing secretKey) {
repository.storeSecretKey(accountId, secretKey)
.compose(schedulers.executeUiSafeCompletable())
.subscribe();
}
@Override
public OpenPgpSecretKeyBackupPassphrase loadBackupPassphrase() {
return repository.loadBackupPassphrase(accountId)
.compose(schedulers.executeUiSafeSingle())
.blockingGet();
}
@Override
public void storeBackupPassphrase(OpenPgpSecretKeyBackupPassphrase passphrase) {
repository.storeBackupPassphrase(accountId, passphrase)
.compose(schedulers.executeUiSafeCompletable())
.subscribe();
}
}

View File

@ -0,0 +1,54 @@
package org.mercury_im.messenger.core.crypto.ikey;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.ikey.IkeyManager;
import org.jivesoftware.smackx.ikey_ox.OxIkeyManager;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.pgpainless.PGPainless;
import org.pgpainless.key.collection.PGPKeyRing;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import javax.inject.Inject;
public class MercuryIkeyManager {
private final IkeyRepository ikeyRepository;
private final SchedulersFacade schedulers;
@Inject
public MercuryIkeyManager(IkeyRepository ikeyRepository, SchedulersFacade schedulers) {
this.ikeyRepository = ikeyRepository;
this.schedulers = schedulers;
}
public void initFor(MercuryConnection connection)
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException,
InterruptedException, SmackException.NoResponseException,
SmackException.NotConnectedException, SmackException.FeatureNotSupportedException,
XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, IOException {
IkeyManager ikeyManager = IkeyManager.getInstanceFor(connection.getConnection());
// bind repo to store
IkeyStoreAdapter store = new IkeyStoreAdapter(connection.getAccountId(), ikeyRepository, schedulers);
ikeyManager.setStore(store);
PGPSecretKeyRing ikey = store.loadSecretKey();
if (ikey == null) {
PGPKeyRing keyRing = PGPainless.generateKeyRing().simpleEcKeyRing("xmpp:" + connection.getAccount().getAddress());
store.storeSecretKey(keyRing.getSecretKeys());
}
OxIkeyManager oxIkeyManager = OxIkeyManager.getInstanceFor(connection.getConnection());
OpenPgpSecretKeyBackupPassphrase passphrase = oxIkeyManager.depositSecretIdentityKey(ikey);
}
}

View File

@ -0,0 +1,46 @@
package org.mercury_im.messenger.core.data.repository;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.entity.Account;
import java.util.UUID;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Single;
public interface IkeyKeyRepository {
default Maybe<PGPSecretKeyRing> loadSecretKey(Account account) {
return loadSecretKey(account.getId());
}
Maybe<PGPSecretKeyRing> loadSecretKey(UUID accountId);
default Completable storeSecretKey(Account account, PGPSecretKeyRing secretKey) {
return storeSecretKey(account.getId(), secretKey);
}
Completable storeSecretKey(UUID accountId, PGPSecretKeyRing secretKey);
default Single<Integer> deleteSecretKey(Account account) {
return deleteSecretKey(account.getId());
}
Single<Integer> deleteSecretKey(UUID accountId);
default Single<OpenPgpSecretKeyBackupPassphrase> loadBackupPassphrase(Account account) {
return loadBackupPassphrase(account.getId());
}
Single<OpenPgpSecretKeyBackupPassphrase> loadBackupPassphrase(UUID accountID);
default Completable storeBackupPassphrase(Account account, OpenPgpSecretKeyBackupPassphrase passphrase) {
return storeBackupPassphrase(account.getId(), passphrase);
}
Completable storeBackupPassphrase(UUID accountID, OpenPgpSecretKeyBackupPassphrase passphrase);
}

View File

@ -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<IkeyElement> loadRecord(Account account, EntityBareJid jid) {
return loadRecord(account.getId(), jid);
}
Maybe<IkeyElement> 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);
}

View File

@ -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());
}
}