001/**
002 *
003 * Copyright 2019 Paul Schaub
004 *
005 * This file is part of smack-repl.
006 *
007 * smack-repl is free software; you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as published by
009 * the Free Software Foundation; either version 3 of the License, or
010 * (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU General Public License
018 * along with this program; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
020 */
021package org.igniterealtime.smack.smackrepl;
022
023import java.io.IOException;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.util.Arrays;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Scanner;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032
033import org.jivesoftware.smack.SmackConfiguration;
034import org.jivesoftware.smack.SmackException;
035import org.jivesoftware.smack.SmackException.NotConnectedException;
036import org.jivesoftware.smack.SmackException.NotLoggedInException;
037import org.jivesoftware.smack.XMPPException;
038import org.jivesoftware.smack.packet.Message;
039import org.jivesoftware.smack.packet.MessageBuilder;
040import org.jivesoftware.smack.packet.Stanza;
041import org.jivesoftware.smack.tcp.XMPPTCPConnection;
042import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
043
044import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
045import org.jivesoftware.smackx.muc.MultiUserChat;
046import org.jivesoftware.smackx.omemo.OmemoManager;
047import org.jivesoftware.smackx.omemo.OmemoMessage;
048import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
049import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
050import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
051import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
052import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
053import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
054import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener;
055import org.jivesoftware.smackx.omemo.signal.SignalCachingOmemoStore;
056import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore;
057import org.jivesoftware.smackx.omemo.signal.SignalOmemoService;
058import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
059import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
060import org.jivesoftware.smackx.omemo.trust.TrustState;
061import org.jivesoftware.smackx.pubsub.PubSubException;
062
063import org.jxmpp.jid.BareJid;
064import org.jxmpp.jid.EntityBareJid;
065import org.jxmpp.jid.impl.JidCreate;
066
067public class OmemoClient {
068
069    public static final Logger LOGGER = Logger.getLogger(OmemoClient.class.getName());
070
071    private static final Scanner scanner = new Scanner(System.in, "UTF-8");
072    private final XMPPTCPConnection connection;
073    private final OmemoManager omemoManager;
074
075    public static void main(String[] args)
076            throws XMPPException, SmackException, IOException, InterruptedException, CorruptedOmemoKeyException {
077        SmackConfiguration.DEBUG = true;
078        if (args.length != 2) {
079            print("Missing arguments: <jid> <password>");
080            return;
081        }
082        SignalOmemoService.acknowledgeLicense();
083        SignalOmemoService.setup();
084        SignalOmemoService omemoService = (SignalOmemoService) SignalOmemoService.getInstance();
085        Path omemoStoreDirectory = Files.createTempDirectory("omemo-store");
086        omemoService.setOmemoStoreBackend(new SignalCachingOmemoStore(new SignalFileBasedOmemoStore(omemoStoreDirectory.toFile())));
087
088        EntityBareJid jid = JidCreate.entityBareFromOrThrowUnchecked(args[0]);
089        String password = args[1];
090        OmemoClient client = new OmemoClient(jid, password);
091        try {
092            client.start();
093
094            while (true) {
095                String input = scanner.nextLine();
096                if (input.startsWith("/quit")) {
097                    break;
098                }
099                if (input.isEmpty()) {
100                    continue;
101                }
102                client.handleInput(input);
103            }
104        } finally {
105            client.stop();
106        }
107    }
108
109    public OmemoClient(EntityBareJid jid, String password) {
110        connection = new XMPPTCPConnection(XMPPTCPConnectionConfiguration.builder()
111                .setXmppAddressAndPassword(jid, password).build());
112        connection.setReplyTimeout(10 * 1000);
113        omemoManager = OmemoManager.getInstanceFor(connection);
114        omemoManager.setTrustCallback(new OmemoTrustCallback() {
115            // In a real app you'd want to persist these decisions
116            private final Map<OmemoFingerprint, TrustState> trustStateMap = new HashMap<>();
117            @Override
118            public TrustState getTrust(OmemoDevice device, OmemoFingerprint fingerprint) {
119                return trustStateMap.get(fingerprint) != null ? trustStateMap.get(fingerprint) : TrustState.undecided;
120            }
121
122            @Override
123            public void setTrust(OmemoDevice device, OmemoFingerprint fingerprint, TrustState state) {
124                trustStateMap.put(fingerprint, state);
125            }
126        });
127        omemoManager.addOmemoMessageListener(new OmemoMessageListener() {
128            @Override
129            public void onOmemoMessageReceived(Stanza s, OmemoMessage.Received m) {
130                print(m.getSenderDevice() + ": " + (m.getBody() != null ? m.getBody() : "<keyTransportMessage>"));
131            }
132
133            @Override
134            public void onOmemoCarbonCopyReceived(CarbonExtension.Direction d, Message cc, Message wm, OmemoMessage.Received m) {
135                onOmemoMessageReceived(cc, m);
136            }
137        });
138        omemoManager.addOmemoMucMessageListener(new OmemoMucMessageListener() {
139            @Override
140            public void onOmemoMucMessageReceived(MultiUserChat muc, Stanza s, OmemoMessage.Received m) {
141                print(s.getFrom() + ":" + m.getSenderDevice().getDeviceId() + ": " + (m.getBody() != null ? m.getBody() : "<keyTransportMessage>"));
142            }
143        });
144    }
145
146    public void start()
147            throws XMPPException, SmackException, IOException, InterruptedException, CorruptedOmemoKeyException {
148        connection.connect().login();
149        omemoManager.initialize();
150        print("Logged in!");
151    }
152
153    public void stop() {
154        connection.disconnect();
155    }
156
157    public void handleInput(String input)
158            throws NotConnectedException, NotLoggedInException, InterruptedException, IOException {
159        String[] com = input.split(" ", 3);
160        switch (com[0]) {
161            case "/omemo":
162                if (com.length < 3) {
163                    print("Usage: /omemo <contact-jid> <message>");
164                    return;
165                }
166
167                BareJid recipient = JidCreate.bareFrom(com[1]);
168                String body = com[2];
169
170                MessageBuilder messageBuilder = connection.getStanzaFactory().buildMessageStanza();
171                try {
172                    Message omemoMessage = omemoManager.encrypt(recipient, body).buildMessage(messageBuilder, recipient);
173                    connection.sendStanza(omemoMessage);
174                } catch (UndecidedOmemoIdentityException e) {
175                    print("Undecided Identities!\n" + Arrays.toString(e.getUndecidedDevices().toArray()));
176                } catch (CryptoFailedException | SmackException.NoResponseException e) {
177                    LOGGER.log(Level.SEVERE, "Unexpected Exception", e);
178                }
179                break;
180            case "/trust":
181                print("Trust");
182                if (com.length != 2) {
183                    print("Usage: /trust <contact-jid>");
184                }
185
186                BareJid contact = JidCreate.bareFrom(com[1]);
187
188                HashMap<OmemoDevice, OmemoFingerprint> devices;
189                try {
190                    devices = omemoManager.getActiveFingerprints(contact);
191                } catch (CorruptedOmemoKeyException | CannotEstablishOmemoSessionException | SmackException.NoResponseException e) {
192                    LOGGER.log(Level.SEVERE, "Unexpected Exception", e);
193                    return;
194                }
195                for (OmemoDevice d : devices.keySet()) {
196                    print("Trust (1) or distrust (2)?\n" + devices.get(d).blocksOf8Chars());
197                    if (Integer.parseInt(scanner.nextLine()) == 1) {
198                        omemoManager.trustOmemoIdentity(d, devices.get(d));
199                    } else {
200                        omemoManager.distrustOmemoIdentity(d, devices.get(d));
201                    }
202                }
203                print("Done.");
204                break;
205            case "/purge":
206                try {
207                    omemoManager.purgeDeviceList();
208                    print("Purged.");
209                } catch (XMPPException.XMPPErrorException | SmackException.NoResponseException | PubSubException.NotALeafNodeException e) {
210                    LOGGER.log(Level.SEVERE, "Unexpected Exception", e);
211                }
212        }
213    }
214
215    private static void print(String msg) {
216        // CHECKSTYLE:OFF
217        System.out.println(msg);
218        // CHECKSTYLE:ON
219    }
220}