/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.encryption; import java.util.*; import java.util.regex.*; import util.xml.XML; import com.leafdigital.irc.api.*; import com.leafdigital.ircui.api.*; import com.leafdigital.ui.api.UI; import leafchat.core.api.*; /** * Encryption plugin. Used for encrypted chat via custom CTCP messages. */ public class EncryptionPlugin implements Plugin { static final String ALGORITHM_CIPHER="TripleDES"; static final String ALGORITHM_KEYAGREEMENT="DH"; private PluginContext context; /** Map of Server => String [LC nick] => EncryptionState */ private Map<Server, Map<String, EncryptedWindow>> servers = null; @Override public void init(PluginContext context, PluginLoadReporter reporter) throws GeneralException { this.context=context; context.requestMessages(UserCTCPRequestIRCMsg.class,this); context.requestMessages(UserCTCPResponseIRCMsg.class,this); context.requestMessages(UserCommandMsg.class,this); context.requestMessages(UserCommandListMsg.class,this); context.requestMessages(IRCActionListMsg.class,this); } final static String CTCP_TEXT="ENCRYPTEDTEXT",CTCP_NICK="ENCRYPTEDNICKCHANGE", CTCP_INIT="ENCRYPTEDSTART",CTCP_END="ENCRYPTEDSTOP",CTCP_INFO="ENCRYPTEDINFO"; /** * Message: User command (used to handle /encryptedquery). * @param msg Message */ public synchronized void msg(UserCommandMsg msg) { if("encryptedquery".equals(msg.getCommand())) { // Check they've entered only a single nick if(!msg.getParams().matches("[^ ]+")) { msg.getMessageDisplay().showError("Syntax: /encryptedquery PersonYouWantToTalkTo"); msg.markHandled(); return; } String nick=msg.getParams(); // Check they aren't trying to talk to themselves if(nick.equalsIgnoreCase(msg.getServer().getOurNick())) { msg.getMessageDisplay().showError("You cannot start an encrypted chat with yourself."); msg.markHandled(); return; } if(getWindow(msg.getServer(),nick)!=null) { msg.getMessageDisplay().showError("An encrypted chat window is already open to that nick. Close the window if you want to start again."); msg.markHandled(); return; } // Open the window EncryptedWindow w=new EncryptedWindow(context,msg.getServer(),nick); addSession(msg.getServer(),nick,w); w.initLocal(); msg.markHandled(); } } /** * Message: Listing available commands. * @param msg Message */ public void msg(UserCommandListMsg msg) { msg.addCommand(true, "encryptedquery", UserCommandListMsg.FREQ_UNCOMMON, "/encryptedquery <nick>", "Open a securely-encrypted chat window with the named user"); } /** * Message: IRC action list (used to add encrypted chat option to menu when * you click on a single user). * @param msg Message */ public synchronized void msg(IRCActionListMsg msg) { if(!msg.hasSingleNick()) return; final String nick=msg.getSingleNick(); if(nick.equalsIgnoreCase(msg.getServer().getOurNick())) return; if(getWindow(msg.getServer(),nick)!=null) return; msg.addIRCAction(new IRCAction() { @Override public int getCategory() { return CATEGORY_USER; } @Override public String getName() { return "Encrypted chat with "+nick; } @Override public int getOrder() { return 700; } @Override public void run(Server s,String contextChannel,String contextNick,String selectedChannel,String[] selectedNicks,MessageDisplay caller) { if(context.getSingle(UI.class).showOptionalQuestion( "encryption-requires-lc2-warning",null,"Encrypted chat requires leafChat 2", "Encrypted chat only works if the other person is using leafChat 2. " + "If you haven't checked, please make sure that they are leafChat " + "users before continuing.",UI.BUTTON_YES|UI.BUTTON_CANCEL, "Start encrypted chat",null,null,UI.BUTTON_CANCEL)==UI.BUTTON_YES) { context.getSingle(Commands.class).doCommand( "/encryptedquery "+nick,s,null,null,caller,false); } } }); } private synchronized void addSession(Server s, String nick, EncryptedWindow w) { if(servers==null) servers = new HashMap<Server, Map<String, EncryptedWindow>>(); Map<String, EncryptedWindow> sessions = servers.get(s); if(sessions==null) { sessions = new HashMap<String, EncryptedWindow>(); servers.put(s,sessions); } sessions.put(nick.toLowerCase(),w); } synchronized void killSession(Server s, EncryptedWindow w) { if(servers==null) return; Map<String, EncryptedWindow> sessions = servers.get(s); if(sessions==null) return; for(Iterator<EncryptedWindow> i=sessions.values().iterator(); i.hasNext(); ) { if(i.next()==w) { i.remove(); } } if(sessions.isEmpty()) servers.remove(s); if(servers.isEmpty()) servers=null; } private static final Pattern KEYPARTS=Pattern.compile("([0-9]+)/([0-9]+)"); /** * Message: CTCP request. Used to detect the CTCP messages for encrypted * chat. * @param msg Message */ public void msg(UserCTCPRequestIRCMsg msg) { String nick=msg.getSourceUser().getNick(); EncryptedWindow w=getWindow(msg.getServer(),nick); if(msg.getRequest().equals(CTCP_INIT)) { String[] params=IRCMsg.convertISO(msg.getText()).split(" ",3); Matcher m=KEYPARTS.matcher(params[1]); if(params.length!=3 || !m.matches()) { getMessageDisplay(msg.getServer()).showError( "Received encrypted-message request from <nick>"+XML.esc(msg.getSourceUser().getNick())+ "</nick> in invalid format."); msg.markHandled(); return; } int partNum=Integer.parseInt(m.group(1)), partTotal=Integer.parseInt(m.group(2)); if(!params[0].equals("DH/TripleDES")) { if(partNum==1) { getMessageDisplay(msg.getServer()).showError( "Received encrypted-message request from <nick>"+XML.esc(msg.getSourceUser().getNick())+ "</nick> with unsupported encryption format <key>"+XML.esc(params[0])+"</key>."); msg.getServer().sendLine(IRCMsg.constructBytes("NOTICE " +nick+" :\u0001ENCRYPTEDFORMATUNSUPPORTED "+ params[0]+"\u0001")); } msg.markHandled(); return; } if(partNum==1) { w=new EncryptedWindow(context,msg.getServer(),nick); addSession(msg.getServer(),nick,w); } else if(w==null) { getMessageDisplay(msg.getServer()).showError( "Received encrypted-message request from <nick>"+XML.esc(msg.getSourceUser().getNick())+ "</nick> in invalid format."); msg.markHandled(); return; } w.initRemote(params[2],partNum,partTotal); msg.markHandled(); return; } if(msg.getRequest().equals(CTCP_NICK)) { String[] params=IRCMsg.convertISO(msg.getText()).split(" ",3); if(params.length!=1) { getMessageDisplay(msg.getServer()).showError( "Received encrypted nick change from <nick>"+msg.getSourceUser().getNick()+ "</nick> in invalid format."); msg.markHandled(); return; } w=getWindow(msg.getServer(),params[0]); if(w==null) { getMessageDisplay(msg.getServer()).showError( "Received encrypted nick change from <nick>"+msg.getSourceUser().getNick()+ "</nick> for window that no longer exists."); msg.markHandled(); return; } w.nick(nick); Map<String, EncryptedWindow> nicks = servers.get(msg.getServer()); nicks.remove(params[0].toLowerCase()); nicks.put(nick,w); msg.markHandled(); return; } if(msg.getRequest().equals(CTCP_TEXT)) { String[] params=IRCMsg.convertISO(msg.getText()).split(" ",3); if(params.length!=2 || !params[0].matches("[AS]")) { getMessageDisplay(msg.getServer()).showError( "Received encrypted text from <nick>"+msg.getSourceUser().getNick()+ "</nick> in invalid format."); msg.markHandled(); return; } if(w==null) { getMessageDisplay(msg.getServer()).showError( "Received encrypted text from <nick>"+msg.getSourceUser().getNick()+ "</nick> for window that no longer exists."); } else { w.text(params[0].equals("A"),params[1]); } msg.markHandled(); return; } if(msg.getRequest().equals(CTCP_END)) { // Just ignore if we get one of these after window was already closed if(w!=null) w.end(); msg.markHandled(); return; } if(msg.getRequest().equals(CTCP_INFO)) { msg.markHandled(); return; } } private MessageDisplay getMessageDisplay(Server s) { return context.getSingle(IRCUI.class).getMessageDisplay(s); } /** * Message: CTCP response. Used to handle responses to encrypted chat. * @param msg Message */ public void msg(UserCTCPResponseIRCMsg msg) { if(msg.getRequest().equals(CTCP_INIT)) { String nick=msg.getSourceUser().getNick(); String[] params=IRCMsg.convertISO(msg.getText()).split(" ",3); Matcher m=KEYPARTS.matcher(params[0]); if(params.length!=2 || !m.matches()) { getMessageDisplay(msg.getServer()).showError( "Received encrypted-message confirmation from <nick>"+XML.esc(msg.getSourceUser().getNick())+ "</nick> in invalid format."); msg.markHandled(); return; } EncryptedWindow w=getWindow(msg.getServer(),nick); if(w==null) { getMessageDisplay(msg.getServer()).showError( "Received encrypted text from <nick>"+msg.getSourceUser().getNick()+ "</nick> for window that no longer exists."); } else { int partNum=Integer.parseInt(m.group(1)), partTotal=Integer.parseInt(m.group(2)); w.finishInit(params[1],partNum,partTotal); } msg.markHandled(); } } private synchronized EncryptedWindow getWindow(Server s,String nick) { if(servers==null) return null; Map<String, EncryptedWindow> sessions = servers.get(s); if(sessions==null) return null; return sessions.get(nick.toLowerCase()); } @Override public void close() throws GeneralException { } @Override public String toString() { // Used to display in system log etc. return "Encryption plugin"; } }