mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-01 01:35:59 +01:00
Implement OpenPgpManager functions
This commit is contained in:
parent
e00075bca9
commit
6e7145801e
11 changed files with 415 additions and 101 deletions
|
@ -0,0 +1,26 @@
|
||||||
|
package org.jivesoftware.smackx.ox.bouncycastle;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||||
|
import org.jxmpp.jid.BareJid;
|
||||||
|
|
||||||
|
public interface BouncyCastleIdentityStore {
|
||||||
|
|
||||||
|
void storePubkeyList(BareJid jid, PublicKeysListElement list) throws FileNotFoundException, IOException;
|
||||||
|
|
||||||
|
PublicKeysListElement loadPubkeyList(BareJid jid) throws FileNotFoundException, IOException;
|
||||||
|
|
||||||
|
void storePublicKeys(BareJid jid, PGPPublicKeyRingCollection keys);
|
||||||
|
|
||||||
|
PGPPublicKeyRingCollection loadPublicKeys(BareJid jid);
|
||||||
|
|
||||||
|
void storeSecretKeys(PGPSecretKeyRingCollection secretKeys);
|
||||||
|
|
||||||
|
PGPSecretKeyRingCollection loadSecretKeys();
|
||||||
|
|
||||||
|
}
|
|
@ -36,7 +36,6 @@ import org.jivesoftware.smackx.ox.element.PubkeyElement;
|
||||||
|
|
||||||
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.BouncyGPG;
|
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.BouncyGPG;
|
||||||
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PublicKeySize;
|
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PublicKeySize;
|
||||||
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy;
|
|
||||||
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks;
|
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks;
|
||||||
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.XmppKeySelectionStrategy;
|
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.XmppKeySelectionStrategy;
|
||||||
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring;
|
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring;
|
||||||
|
@ -47,6 +46,7 @@ import org.bouncycastle.openpgp.PGPKeyRingGenerator;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
import org.bouncycastle.util.io.Streams;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.jxmpp.jid.BareJid;
|
import org.jxmpp.jid.BareJid;
|
||||||
|
|
||||||
|
@ -56,51 +56,26 @@ public class BouncycastleOpenPgpProvider implements OpenPgpProvider {
|
||||||
private final InMemoryKeyring ourKeys;
|
private final InMemoryKeyring ourKeys;
|
||||||
private final Long ourKeyId;
|
private final Long ourKeyId;
|
||||||
private final Map<BareJid, InMemoryKeyring> theirKeys = new HashMap<>();
|
private final Map<BareJid, InMemoryKeyring> theirKeys = new HashMap<>();
|
||||||
private final KeySelectionStrategy keySelectionStrategy = new XmppKeySelectionStrategy(new Date());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception {
|
|
||||||
InMemoryKeyring decryptionConfig = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys());
|
|
||||||
|
|
||||||
// Add our keys to decryption config
|
|
||||||
for (PGPPublicKeyRing p : ourKeys.getPublicKeyRings()) {
|
|
||||||
decryptionConfig.addPublicKey(p.getPublicKey().getEncoded());
|
|
||||||
}
|
|
||||||
for (PGPSecretKeyRing s : ourKeys.getSecretKeyRings()) {
|
|
||||||
decryptionConfig.addSecretKey(s.getSecretKey().getEncoded());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add their keys to decryption config
|
|
||||||
for (PGPPublicKeyRing p : theirKeys.get(sender).getPublicKeyRings()) {
|
|
||||||
decryptionConfig.addPublicKey(p.getPublicKey().getEncoded());
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayInputStream encryptedIn = new ByteArrayInputStream(
|
|
||||||
element.getEncryptedBase64MessageContent().getBytes(Charset.forName("UTF-8")));
|
|
||||||
|
|
||||||
InputStream decrypted = BouncyGPG.decryptAndVerifyStream()
|
|
||||||
.withConfig(decryptionConfig)
|
|
||||||
.withKeySelectionStrategy(new XmppKeySelectionStrategy(new Date()))
|
|
||||||
.andValidateSomeoneSigned()
|
|
||||||
.fromEncryptedInputStream(encryptedIn);
|
|
||||||
|
|
||||||
ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
Streams.pipeAll(decrypted, decryptedOut);
|
|
||||||
|
|
||||||
return new OpenPgpMessage(null, new String(decryptedOut.toByteArray(), Charset.forName("UTF-8")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public BouncycastleOpenPgpProvider(BareJid ourJid) throws IOException, PGPException, NoSuchAlgorithmException {
|
public BouncycastleOpenPgpProvider(BareJid ourJid) throws IOException, PGPException, NoSuchAlgorithmException {
|
||||||
this.ourJid = ourJid;
|
this.ourJid = ourJid;
|
||||||
PGPSecretKeyRing ourKey = generateKey(ourJid, null).generateSecretKeyRing();
|
PGPSecretKeyRing ourKey = generateKey(ourJid).generateSecretKeyRing();
|
||||||
ourKeyId = ourKey.getPublicKey().getKeyID();
|
ourKeyId = ourKey.getPublicKey().getKeyID();
|
||||||
ourKeys = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys());
|
ourKeys = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys());
|
||||||
ourKeys.addSecretKey(ourKey.getSecretKey().getEncoded());
|
ourKeys.addSecretKey(ourKey.getSecretKey().getEncoded());
|
||||||
ourKeys.addPublicKey(ourKey.getPublicKey().getEncoded());
|
ourKeys.addPublicKey(ourKey.getPublicKey().getEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processRecipientsPublicKey(BareJid jid, PubkeyElement element) throws IOException, PGPException {
|
@Override
|
||||||
|
public PubkeyElement createPubkeyElement() throws IOException, PGPException {
|
||||||
|
PGPPublicKey pubKey = ourKeys.getPublicKeyRings().getPublicKey(ourKeyId);
|
||||||
|
PubkeyElement.PubkeyDataElement dataElement = new PubkeyElement.PubkeyDataElement(
|
||||||
|
Base64.encode(pubKey.getEncoded()));
|
||||||
|
return new PubkeyElement(dataElement, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processPubkeyElement(PubkeyElement element, BareJid jid) throws IOException, PGPException {
|
||||||
byte[] decoded = Base64.decode(element.getDataElement().getB64Data());
|
byte[] decoded = Base64.decode(element.getDataElement().getB64Data());
|
||||||
|
|
||||||
InMemoryKeyring contactsKeyring = theirKeys.get(jid);
|
InMemoryKeyring contactsKeyring = theirKeys.get(jid);
|
||||||
|
@ -112,13 +87,6 @@ public class BouncycastleOpenPgpProvider implements OpenPgpProvider {
|
||||||
contactsKeyring.addPublicKey(decoded);
|
contactsKeyring.addPublicKey(decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PubkeyElement createPubkeyElement() throws IOException, PGPException {
|
|
||||||
PGPPublicKey pubKey = ourKeys.getPublicKeyRings().getPublicKey(ourKeyId);
|
|
||||||
PubkeyElement.PubkeyDataElement dataElement = new PubkeyElement.PubkeyDataElement(
|
|
||||||
Base64.encode(pubKey.getEncoded()));
|
|
||||||
return new PubkeyElement(dataElement, new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OpenPgpElement signAndEncrypt(InputStream inputStream, Set<BareJid> recipients)
|
public OpenPgpElement signAndEncrypt(InputStream inputStream, Set<BareJid> recipients)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
@ -170,12 +138,52 @@ public class BouncycastleOpenPgpProvider implements OpenPgpProvider {
|
||||||
return new OpenPgpElement(base64);
|
return new OpenPgpElement(base64);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PGPKeyRingGenerator generateKey(BareJid owner, char[] passPhrase) throws NoSuchAlgorithmException, PGPException {
|
@Override
|
||||||
|
public OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception {
|
||||||
|
InMemoryKeyring decryptionConfig = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys());
|
||||||
|
|
||||||
|
// Add our keys to decryption config
|
||||||
|
for (PGPPublicKeyRing p : ourKeys.getPublicKeyRings()) {
|
||||||
|
decryptionConfig.addPublicKey(p.getPublicKey().getEncoded());
|
||||||
|
}
|
||||||
|
for (PGPSecretKeyRing s : ourKeys.getSecretKeyRings()) {
|
||||||
|
decryptionConfig.addSecretKey(s.getSecretKey().getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add their keys to decryption config
|
||||||
|
for (PGPPublicKeyRing p : theirKeys.get(sender).getPublicKeyRings()) {
|
||||||
|
decryptionConfig.addPublicKey(p.getPublicKey().getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayInputStream encryptedIn = new ByteArrayInputStream(
|
||||||
|
element.getEncryptedBase64MessageContent().getBytes(Charset.forName("UTF-8")));
|
||||||
|
|
||||||
|
InputStream decrypted = BouncyGPG.decryptAndVerifyStream()
|
||||||
|
.withConfig(decryptionConfig)
|
||||||
|
.withKeySelectionStrategy(new XmppKeySelectionStrategy(new Date()))
|
||||||
|
.andValidateSomeoneSigned()
|
||||||
|
.fromEncryptedInputStream(encryptedIn);
|
||||||
|
|
||||||
|
ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
Streams.pipeAll(decrypted, decryptedOut);
|
||||||
|
|
||||||
|
return new OpenPgpMessage(null, new String(decryptedOut.toByteArray(), Charset.forName("UTF-8")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFingerprint() throws IOException, PGPException {
|
||||||
|
return new String(Hex.encode(ourKeys.getKeyFingerPrintCalculator()
|
||||||
|
.calculateFingerprint(ourKeys.getPublicKeyRings().getPublicKey(ourKeyId)
|
||||||
|
.getPublicKeyPacket())), Charset.forName("UTF-8")).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PGPKeyRingGenerator generateKey(BareJid owner) throws NoSuchAlgorithmException, PGPException {
|
||||||
PGPKeyRingGenerator generator = BouncyGPG.createKeyPair()
|
PGPKeyRingGenerator generator = BouncyGPG.createKeyPair()
|
||||||
.withRSAKeys()
|
.withRSAKeys()
|
||||||
.ofSize(PublicKeySize.RSA._2048)
|
.ofSize(PublicKeySize.RSA._2048)
|
||||||
.forIdentity("xmpp:" + owner.toString())
|
.forIdentity("xmpp:" + owner.toString())
|
||||||
.withPassphrase(passPhrase)
|
.withoutPassphrase()
|
||||||
.build();
|
.build();
|
||||||
return generator;
|
return generator;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package org.jivesoftware.smackx.ox.bouncycastle;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.util.Objects;
|
||||||
|
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||||
|
import org.jxmpp.jid.BareJid;
|
||||||
|
import org.jxmpp.util.XmppDateTime;
|
||||||
|
|
||||||
|
public class FileBasedBouncyCastleIdentityStore implements BouncyCastleIdentityStore {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(FileBasedBouncyCastleIdentityStore.class.getName());
|
||||||
|
|
||||||
|
private final File baseDirectory;
|
||||||
|
|
||||||
|
public FileBasedBouncyCastleIdentityStore(File path) {
|
||||||
|
// Check if path is not null, not a file and directory exists
|
||||||
|
if (!Objects.requireNonNull(path).exists()) {
|
||||||
|
path.mkdirs();
|
||||||
|
} else if (path.isFile()) {
|
||||||
|
throw new IllegalArgumentException("Path MUST point to a directory, not a file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseDirectory = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storePubkeyList(BareJid jid, PublicKeysListElement list) throws IOException {
|
||||||
|
File contactsDir = contactsDir(jid);
|
||||||
|
File destination = new File(contactsDir, "pubkey_list");
|
||||||
|
DataOutputStream dataOut = new DataOutputStream(new FileOutputStream(destination));
|
||||||
|
|
||||||
|
for (String key : list.getMetadata().keySet()) {
|
||||||
|
PublicKeysListElement.PubkeyMetadataElement value = list.getMetadata().get(key);
|
||||||
|
dataOut.writeUTF(value.getV4Fingerprint());
|
||||||
|
dataOut.writeUTF(XmppDateTime.formatXEP0082Date(value.getDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
dataOut.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeysListElement loadPubkeyList(BareJid jid) throws IOException {
|
||||||
|
File contactsDir = contactsDir(jid);
|
||||||
|
File source = new File(contactsDir, "pubkey_list");
|
||||||
|
DataInputStream dataIn = new DataInputStream(new FileInputStream(source));
|
||||||
|
|
||||||
|
PublicKeysListElement.Builder builder = PublicKeysListElement.builder();
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
String fingerprint = dataIn.readUTF();
|
||||||
|
String d = dataIn.readUTF();
|
||||||
|
Date date = XmppDateTime.parseXEP0082Date(d);
|
||||||
|
builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, date));
|
||||||
|
} catch (ParseException e) {
|
||||||
|
LOGGER.log(Level.WARNING, "Could not parse date.", e);
|
||||||
|
} catch (EOFException e) {
|
||||||
|
LOGGER.log(Level.INFO, "Reached EOF.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storePublicKeys(BareJid jid, PGPPublicKeyRingCollection keys) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PGPPublicKeyRingCollection loadPublicKeys(BareJid jid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeSecretKeys(PGPSecretKeyRingCollection secretKeys) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PGPSecretKeyRingCollection loadSecretKeys() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File contactsDir(BareJid contact) {
|
||||||
|
File f = new File(baseDirectory, "contacts/" + contact.toString());
|
||||||
|
if (!f.exists()) {
|
||||||
|
f.mkdirs();
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
|
@ -116,8 +116,8 @@ public class BasicEncryptionTest extends SmackTestSuite {
|
||||||
throws IOException, PGPException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException {
|
throws IOException, PGPException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException {
|
||||||
final String alice = "alice@wonderland.lit";
|
final String alice = "alice@wonderland.lit";
|
||||||
final String bob = "bob@builder.tv";
|
final String bob = "bob@builder.tv";
|
||||||
PGPKeyRingGenerator g1 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(alice), null);
|
PGPKeyRingGenerator g1 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(alice));
|
||||||
PGPKeyRingGenerator g2 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(bob), null);
|
PGPKeyRingGenerator g2 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(bob));
|
||||||
PGPSecretKey s1 = g1.generateSecretKeyRing().getSecretKey();
|
PGPSecretKey s1 = g1.generateSecretKeyRing().getSecretKey();
|
||||||
PGPSecretKey s2 = g2.generateSecretKeyRing().getSecretKey();
|
PGPSecretKey s2 = g2.generateSecretKeyRing().getSecretKey();
|
||||||
PGPPublicKey p1 = g1.generatePublicKeyRing().getPublicKey();
|
PGPPublicKey p1 = g1.generatePublicKeyRing().getPublicKey();
|
||||||
|
|
|
@ -52,9 +52,10 @@ public class BouncycastleOpenPgpProviderTest extends SmackTestSuite {
|
||||||
// dry exchange keys
|
// dry exchange keys
|
||||||
PubkeyElement aliceKeys = aliceProvider.createPubkeyElement();
|
PubkeyElement aliceKeys = aliceProvider.createPubkeyElement();
|
||||||
PubkeyElement cheshireKeys = cheshireProvider.createPubkeyElement();
|
PubkeyElement cheshireKeys = cheshireProvider.createPubkeyElement();
|
||||||
aliceProvider.processRecipientsPublicKey(cheshire, cheshireKeys);
|
aliceProvider.processPubkeyElement(cheshireKeys, cheshire);
|
||||||
cheshireProvider.processRecipientsPublicKey(alice, aliceKeys);
|
cheshireProvider.processPubkeyElement(aliceKeys, alice);
|
||||||
|
|
||||||
|
// Create signed and encrypted message from alice to the cheshire cat
|
||||||
SigncryptElement signcryptElement = new SigncryptElement(
|
SigncryptElement signcryptElement = new SigncryptElement(
|
||||||
Collections.<Jid>singleton(cheshire),
|
Collections.<Jid>singleton(cheshire),
|
||||||
Collections.<ExtensionElement>singletonList(
|
Collections.<ExtensionElement>singletonList(
|
||||||
|
@ -63,10 +64,11 @@ public class BouncycastleOpenPgpProviderTest extends SmackTestSuite {
|
||||||
signcryptElement.toInputStream(),
|
signcryptElement.toInputStream(),
|
||||||
Collections.singleton(cheshire));
|
Collections.singleton(cheshire));
|
||||||
|
|
||||||
|
// Decrypt the message as the cheshire cat
|
||||||
OpenPgpMessage decrypted = cheshireProvider.decryptAndVerify(encrypted, alice);
|
OpenPgpMessage decrypted = cheshireProvider.decryptAndVerify(encrypted, alice);
|
||||||
OpenPgpContentElement content = decrypted.getOpenPgpContentElement();
|
OpenPgpContentElement content = decrypted.getOpenPgpContentElement();
|
||||||
assertTrue(content instanceof SigncryptElement);
|
|
||||||
|
|
||||||
|
assertTrue(content instanceof SigncryptElement);
|
||||||
assertXMLEqual(signcryptElement.toXML(null).toString(), content.toXML(null).toString());
|
assertXMLEqual(signcryptElement.toXML(null).toString(), content.toXML(null).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package org.jivesoftware.smackx.ox.bouncycastle;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertEquals;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.test.util.SmackTestSuite;
|
||||||
|
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.jxmpp.jid.BareJid;
|
||||||
|
import org.jxmpp.jid.impl.JidCreate;
|
||||||
|
import org.jxmpp.util.XmppDateTime;
|
||||||
|
|
||||||
|
public class FileBasedBouncyCastleIdentityStoreTest extends SmackTestSuite {
|
||||||
|
|
||||||
|
public static File storePath;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setup() {
|
||||||
|
String userHome = System.getProperty("user.home");
|
||||||
|
if (userHome != null) {
|
||||||
|
File f = new File(userHome);
|
||||||
|
storePath = new File(f, ".config/smack-integration-test/ox_store");
|
||||||
|
} else {
|
||||||
|
storePath = new File("int_test_ox_store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
deleteStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void after() {
|
||||||
|
deleteStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteStore() {
|
||||||
|
File[] currList;
|
||||||
|
Stack<File> stack = new Stack<>();
|
||||||
|
stack.push(storePath);
|
||||||
|
while (!stack.isEmpty()) {
|
||||||
|
if (stack.lastElement().isDirectory()) {
|
||||||
|
currList = stack.lastElement().listFiles();
|
||||||
|
if (currList != null && currList.length > 0) {
|
||||||
|
for (File curr : currList) {
|
||||||
|
stack.push(curr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stack.pop().delete();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stack.pop().delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void writeReadPublicKeysLists() throws ParseException, IOException {
|
||||||
|
BareJid jid = JidCreate.bareFrom("edward@snowden.org");
|
||||||
|
String fp1 = "1357B01865B2503C18453D208CAC2A9678548E35";
|
||||||
|
String fp2 = "67819B343B2AB70DED9320872C6464AF2A8E4C02";
|
||||||
|
Date d1 = XmppDateTime.parseDate("2018-03-01T15:26:12Z");
|
||||||
|
Date d2 = XmppDateTime.parseDate("1953-05-16T12:00:00Z");
|
||||||
|
|
||||||
|
PublicKeysListElement list = PublicKeysListElement.builder()
|
||||||
|
.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fp1, d1))
|
||||||
|
.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fp2, d2))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
FileBasedBouncyCastleIdentityStore store = new FileBasedBouncyCastleIdentityStore(storePath);
|
||||||
|
store.storePubkeyList(jid, list);
|
||||||
|
|
||||||
|
PublicKeysListElement retrieved = store.loadPubkeyList(jid);
|
||||||
|
assertEquals(list.getMetadata(), retrieved.getMetadata());
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ public class KeyGenerationTest {
|
||||||
@Test
|
@Test
|
||||||
public void createSecretKey() throws Exception {
|
public void createSecretKey() throws Exception {
|
||||||
PGPSecretKey secretKey = BouncycastleOpenPgpProvider
|
PGPSecretKey secretKey = BouncycastleOpenPgpProvider
|
||||||
.generateKey(JidCreate.bareFrom("alice@wonderland.lit"), null)
|
.generateKey(JidCreate.bareFrom("alice@wonderland.lit"))
|
||||||
.generateSecretKeyRing()
|
.generateSecretKeyRing()
|
||||||
.getSecretKey();
|
.getSecretKey();
|
||||||
assertEquals(PublicKeyType.RSA_GENERAL.getId(), secretKey.getPublicKey().getAlgorithm());
|
assertEquals(PublicKeyType.RSA_GENERAL.getId(), secretKey.getPublicKey().getAlgorithm());
|
||||||
|
|
|
@ -5,6 +5,7 @@ Smack API for XEP-0373: OpenPGP for XMPP."""
|
||||||
// sourceSet.test of the core subproject
|
// sourceSet.test of the core subproject
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':smack-core')
|
compile project(':smack-core')
|
||||||
|
compile project(':smack-extensions')
|
||||||
testCompile project(path: ":smack-core", configuration: "testRuntime")
|
testCompile project(path: ":smack-core", configuration: "testRuntime")
|
||||||
testCompile project(path: ":smack-core", configuration: "archives")
|
testCompile project(path: ":smack-core", configuration: "archives")
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,27 +16,50 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.ox;
|
package org.jivesoftware.smackx.ox;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
import org.jivesoftware.smack.Manager;
|
import org.jivesoftware.smack.Manager;
|
||||||
|
import org.jivesoftware.smack.SmackException;
|
||||||
import org.jivesoftware.smack.XMPPConnection;
|
import org.jivesoftware.smack.XMPPConnection;
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
|
import org.jivesoftware.smack.packet.Message;
|
||||||
|
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
|
||||||
|
import org.jivesoftware.smackx.ox.element.PubkeyElement;
|
||||||
|
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
|
||||||
|
import org.jivesoftware.smackx.pep.PEPListener;
|
||||||
|
import org.jivesoftware.smackx.pep.PEPManager;
|
||||||
|
import org.jivesoftware.smackx.pubsub.EventElement;
|
||||||
|
import org.jivesoftware.smackx.pubsub.Item;
|
||||||
|
import org.jivesoftware.smackx.pubsub.LeafNode;
|
||||||
|
import org.jivesoftware.smackx.pubsub.PayloadItem;
|
||||||
|
import org.jivesoftware.smackx.pubsub.PubSubException;
|
||||||
|
import org.jivesoftware.smackx.pubsub.PubSubManager;
|
||||||
|
|
||||||
import org.jxmpp.jid.BareJid;
|
import org.jxmpp.jid.BareJid;
|
||||||
|
import org.jxmpp.jid.EntityBareJid;
|
||||||
|
|
||||||
public final class OpenPgpManager extends Manager {
|
public final class OpenPgpManager extends Manager implements PEPListener {
|
||||||
|
|
||||||
public static final String PEP_NODE_PUBLIC_KEYS = "urn:xmpp:openpgp:0:public-keys";
|
public static final String PEP_NODE_PUBLIC_KEYS = "urn:xmpp:openpgp:0:public-keys";
|
||||||
|
public static final String PEP_NODE_PUBLIC_KEYS_NOTIFY = PEP_NODE_PUBLIC_KEYS + "+notify";
|
||||||
|
|
||||||
public static String PEP_NODE_PUBLIC_KEY(String id) {
|
public static String PEP_NODE_PUBLIC_KEY(String id) {
|
||||||
return PEP_NODE_PUBLIC_KEYS + ":" + id;
|
return PEP_NODE_PUBLIC_KEYS + ":" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Map<XMPPConnection, OpenPgpManager> INSTANCES = new WeakHashMap<>();
|
private static final Map<XMPPConnection, OpenPgpManager> INSTANCES = new WeakHashMap<>();
|
||||||
|
private OpenPgpProvider provider;
|
||||||
|
|
||||||
private OpenPgpManager(XMPPConnection connection) {
|
private OpenPgpManager(XMPPConnection connection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
|
|
||||||
|
// Subscribe to public key changes
|
||||||
|
PEPManager.getInstanceFor(connection()).addPEPListener(this);
|
||||||
|
ServiceDiscoveryManager.getInstanceFor(connection())
|
||||||
|
.addFeature(PEP_NODE_PUBLIC_KEYS_NOTIFY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OpenPgpManager getInstanceFor(XMPPConnection connection) {
|
public static OpenPgpManager getInstanceFor(XMPPConnection connection) {
|
||||||
|
@ -48,17 +71,108 @@ public final class OpenPgpManager extends Manager {
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String bareJidToIdentity(BareJid jid) {
|
public void setOpenPgpProvider(OpenPgpProvider provider) {
|
||||||
return "xmpp:" + jid.toString();
|
this.provider = provider;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OpenPgpMessage toOpenPgpMessage(InputStream is) {
|
public void publishPublicKey() throws Exception {
|
||||||
|
ensureProviderIsSet();
|
||||||
|
PubkeyElement pubkeyElement = provider.createPubkeyElement();
|
||||||
|
|
||||||
|
String fingerprint = provider.getFingerprint();
|
||||||
|
String keyNodeName = PEP_NODE_PUBLIC_KEY(fingerprint);
|
||||||
|
PubSubManager pm = PubSubManager.getInstance(connection(), connection().getUser().asBareJid());
|
||||||
|
|
||||||
|
// Check if key available at data node
|
||||||
|
// If not, publish key to data node
|
||||||
|
LeafNode keyNode = pm.getOrCreateLeafNode(keyNodeName);
|
||||||
|
List<Item> items = keyNode.getItems(1);
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
keyNode.publish(new PayloadItem<>(pubkeyElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish ID to metadata node
|
||||||
|
LeafNode metadataNode = pm.getOrCreateLeafNode(PEP_NODE_PUBLIC_KEYS);
|
||||||
|
List<PayloadItem<PublicKeysListElement>> metadataItems = metadataNode.getItems(1);
|
||||||
|
|
||||||
|
PublicKeysListElement.Builder builder = PublicKeysListElement.builder();
|
||||||
|
if (!metadataItems.isEmpty() && metadataItems.get(0).getPayload() != null) {
|
||||||
|
// Add old entries back to list.
|
||||||
|
PublicKeysListElement publishedList = metadataItems.get(0).getPayload();
|
||||||
|
for (PublicKeysListElement.PubkeyMetadataElement meta : publishedList.getMetadata().values()) {
|
||||||
|
builder.addMetadata(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, new Date()));
|
||||||
|
|
||||||
|
metadataNode.publish(new PayloadItem<>(builder.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKeysListElement fetchPubkeysList()
|
||||||
|
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
|
||||||
|
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
|
||||||
|
PubSubException.NotAPubSubNodeException {
|
||||||
|
return fetchPubkeysList(connection().getUser().asBareJid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKeysListElement fetchPubkeysList(BareJid jid)
|
||||||
|
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
|
||||||
|
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
|
||||||
|
PubSubException.NotAPubSubNodeException
|
||||||
|
{
|
||||||
|
PubSubManager pm = PubSubManager.getInstance(connection(), jid);
|
||||||
|
|
||||||
|
LeafNode node = pm.getLeafNode(PEP_NODE_PUBLIC_KEYS);
|
||||||
|
List<PayloadItem<PublicKeysListElement>> list = node.getItems(1);
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void publishPublicKey() {
|
return list.get(0).getPayload();
|
||||||
// Check if key available at data node
|
}
|
||||||
// If not, publish key to data node
|
|
||||||
// Publish ID to metadata node
|
public PubkeyElement fetchPubkey(BareJid jid, String v4_fingerprint)
|
||||||
|
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
|
||||||
|
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
|
||||||
|
PubSubException.NotAPubSubNodeException {
|
||||||
|
PubSubManager pm = PubSubManager.getInstance(connection(), jid);
|
||||||
|
|
||||||
|
LeafNode node = pm.getLeafNode(PEP_NODE_PUBLIC_KEY(v4_fingerprint));
|
||||||
|
List<PayloadItem<PubkeyElement>> list = node.getItems(1);
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.get(0).getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void depositSecretKey() {
|
||||||
|
ensureProviderIsSet();
|
||||||
|
// Create key backup by appending serialized unencrypted secret keys.
|
||||||
|
// Encrypt the backup using a random generated password
|
||||||
|
// Publish the backup to the secret key node (whitelist protected)
|
||||||
|
// Display the backup key to the user
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOurFingerprint() throws Exception {
|
||||||
|
ensureProviderIsSet();
|
||||||
|
return provider.getFingerprint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an {@link IllegalStateException} if no {@link OpenPgpProvider} is set.
|
||||||
|
*/
|
||||||
|
private void ensureProviderIsSet() {
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalStateException("No OpenPgpProvider set!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventReceived(EntityBareJid from, EventElement event, Message message) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.io.InputStream;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
|
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
|
||||||
|
import org.jivesoftware.smackx.ox.element.PubkeyElement;
|
||||||
|
|
||||||
import org.jxmpp.jid.BareJid;
|
import org.jxmpp.jid.BareJid;
|
||||||
|
|
||||||
|
@ -28,4 +29,16 @@ public interface OpenPgpProvider {
|
||||||
OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception;
|
OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception;
|
||||||
|
|
||||||
OpenPgpElement signAndEncrypt(InputStream inputStream, Set<BareJid> recipients) throws Exception;
|
OpenPgpElement signAndEncrypt(InputStream inputStream, Set<BareJid> recipients) throws Exception;
|
||||||
|
|
||||||
|
PubkeyElement createPubkeyElement() throws Exception;
|
||||||
|
|
||||||
|
void processPubkeyElement(PubkeyElement element, BareJid from) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the OpenPGP v4-fingerprint of our key in hexadecimal upper case.
|
||||||
|
*
|
||||||
|
* @return fingerprint
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
String getFingerprint() throws Exception;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.ox.element;
|
package org.jivesoftware.smackx.ox.element;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import org.jivesoftware.smack.packet.ExtensionElement;
|
import org.jivesoftware.smack.packet.ExtensionElement;
|
||||||
import org.jivesoftware.smack.util.XmlStringBuilder;
|
import org.jivesoftware.smack.util.XmlStringBuilder;
|
||||||
import org.jivesoftware.smack.util.stringencoder.Base64;
|
|
||||||
import org.jivesoftware.smackx.ox.OpenPgpManager;
|
|
||||||
import org.jivesoftware.smackx.ox.OpenPgpMessage;
|
|
||||||
import org.jivesoftware.smackx.ox.OpenPgpProvider;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
public class OpenPgpElement implements ExtensionElement {
|
public class OpenPgpElement implements ExtensionElement {
|
||||||
|
|
||||||
|
@ -37,35 +27,10 @@ public class OpenPgpElement implements ExtensionElement {
|
||||||
// Represents the OpenPGP message, but encoded using base64.
|
// Represents the OpenPGP message, but encoded using base64.
|
||||||
private final String base64EncodedOpenPgpMessage;
|
private final String base64EncodedOpenPgpMessage;
|
||||||
|
|
||||||
private OpenPgpMessage openPgpMessage;
|
|
||||||
|
|
||||||
// Represents the OpenPGP message, but base64 decoded.
|
|
||||||
private byte[] openPgpMessageBytes;
|
|
||||||
|
|
||||||
private OpenPgpContentElement openPgpContentElement;
|
|
||||||
|
|
||||||
public OpenPgpElement(String base64EncodedOpenPgpMessage) {
|
public OpenPgpElement(String base64EncodedOpenPgpMessage) {
|
||||||
this.base64EncodedOpenPgpMessage = base64EncodedOpenPgpMessage;
|
this.base64EncodedOpenPgpMessage = base64EncodedOpenPgpMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OpenPgpMessage getOpenPgpMessage(OpenPgpProvider provider) {
|
|
||||||
if (openPgpMessage == null) {
|
|
||||||
ensureOpenPgpMessageBytesSet();
|
|
||||||
InputStream is = new ByteArrayInputStream(openPgpMessageBytes);
|
|
||||||
openPgpMessage = OpenPgpManager.toOpenPgpMessage(is);
|
|
||||||
}
|
|
||||||
|
|
||||||
return openPgpMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OpenPgpContentElement getContentElement(OpenPgpProvider provider) throws XmlPullParserException, IOException {
|
|
||||||
if (openPgpContentElement == null) {
|
|
||||||
openPgpContentElement = getOpenPgpMessage(provider).getOpenPgpContentElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
return openPgpContentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEncryptedBase64MessageContent() {
|
public String getEncryptedBase64MessageContent() {
|
||||||
return base64EncodedOpenPgpMessage;
|
return base64EncodedOpenPgpMessage;
|
||||||
}
|
}
|
||||||
|
@ -86,10 +51,4 @@ public class OpenPgpElement implements ExtensionElement {
|
||||||
xml.rightAngleBracket().append(base64EncodedOpenPgpMessage).closeElement(this);
|
xml.rightAngleBracket().append(base64EncodedOpenPgpMessage).closeElement(this);
|
||||||
return xml;
|
return xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureOpenPgpMessageBytesSet() {
|
|
||||||
if (openPgpMessageBytes != null) return;
|
|
||||||
|
|
||||||
openPgpMessageBytes = Base64.decode(base64EncodedOpenPgpMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue