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}