/* 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 2011 Samuel Marshall. */ package com.leafdigital.encryption; import java.io.UnsupportedEncodingException; import java.security.*; import java.security.spec.*; import javax.crypto.*; import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import org.w3c.dom.Element; import com.leafdigital.irc.api.*; import com.leafdigital.ircui.api.*; import util.*; import util.xml.XML; import leafchat.core.api.*; /** * Chat window for encrypted chat. */ public class EncryptedWindow implements GeneralChatWindow.Handler { private PrivateKey firstPartyPrivate; private Cipher encrypter,decrypter; private PluginContext context; private GeneralChatWindow w; private Server s; private String nick; private String ourNick; private boolean gotRemote=false,everGotRemote=false; EncryptedWindow(PluginContext context,Server s,String nick) { this.context=context; this.s=s; this.nick=nick; this.ourNick=s.getOurNick(); w=context.getSingle(IRCUI.class).createGeneralChatWindow( context,this,null,null,null,300,s.getOurNick(),nick,true); w.setEnabled(false); context.requestMessages(NickIRCMsg.class,this); context.requestMessages(ServerDisconnectedMsg.class,this); w.setTitle(nick+" - Encrypted chat"); } /** * Message: Server disconnected. * @param msg Message */ public void msg(ServerDisconnectedMsg msg) { if(msg.getServer()==this) { w.getMessageDisplay().showInfo("Disconnected from server"); ((EncryptionPlugin)context.getPlugin()).killSession(s,this); } } /** * Message: Nickname changed (used to detect changes of our own nickname). * @param msg Message */ public void msg(NickIRCMsg msg) { if(msg.getServer()==s && msg.getSourceUser().getNick().equalsIgnoreCase(ourNick)) { sendCTCP(false,EncryptionPlugin.CTCP_NICK,ourNick); ourNick=msg.getNewNick(); } } void initLocal() { // This bit might take a while so let's call it in another thread w.getMessageDisplay().showInfo("Generating key..."); (new Thread("EncryptedWindow key generation") { @Override public void run() { String key=null; Exception e=null; try { key=initFirstParty(); } catch(GeneralSecurityException gse) { e=gse; } final String finalKey=key; final Exception finalException=e; context.yield(new Runnable() { @Override public void run() { if(finalKey==null) { context.log("Error generating key",finalException); w.getMessageDisplay().showError("Key could not be generated. " + "This is probably because your Java distribution is missing " + "required security features. See system log for details."); } else { w.getMessageDisplay().showInfo("Key generated. Contacting <nick>"+ XML.esc(nick)+"</nick>..."); sendCTCP(false,EncryptionPlugin.CTCP_INFO, ourNick+" is attempting to start an encrypted conversation " + "using a feature built into the leafChat 2 IRC client. " + "If you're using something else, please tell them so. They " + "should have checked with you first!"); int partCount=(finalKey.length()+349)/350; for(int i=0;i<partCount;i++) { sendCTCP(false,EncryptionPlugin.CTCP_INIT,"DH/TripleDES "+ (i+1)+"/"+partCount+" "+finalKey.substring( i*350,Math.min(finalKey.length(),(i+1)*350))); } TimeUtils.addTimedEvent(new Runnable() { @Override public void run() { if(!everGotRemote && w!=null) { w.getMessageDisplay().showError("No response from <nick>"+ XML.esc(nick)+"</nick>. Are you sure they are using " + "leafChat 2? The encrypted chat feature works only with " + "other leafChat 2 users at present."); } } },15000,true); } } }); } }).start(); } private String remoteKey=""; void initRemote(String remoteKeyPart,int partNum,int partTotal) { remoteKey+=remoteKeyPart; if(partNum!=partTotal) return; // This bit might take a while so let's call it in another thread w.getMessageDisplay().showInfo("<nick>"+XML.esc(nick)+"</nick> has requested an encrypted chat. Generating response key..."); (new Thread("EncryptedWindow response key generation") { @Override public void run() { String key=null; Exception e=null; try { key=initSecondParty(remoteKey); } catch(GeneralSecurityException gse) { e=gse; } final String finalKey=key; final Exception finalException=e; context.yield(new Runnable() { @Override public void run() { if(finalKey==null) { context.log("Error generating response key",finalException); w.getMessageDisplay().showError("Key could not be generated. " + "This is probably because your Java distribution is missing " + "required security features. See system log for details."); } else { w.getMessageDisplay().showInfo("Encrypted chat ready."); w.setEnabled(true); int partCount=(finalKey.length()+349)/350; for(int i=0;i<partCount;i++) { sendCTCP(true,EncryptionPlugin.CTCP_INIT, (i+1)+"/"+partCount+" "+finalKey.substring( i*350,Math.min(finalKey.length(),(i+1)*350))); } gotRemote=true; everGotRemote=true; } } }); } }).start(); } void finishInit(String remoteKeyPart,int partNum,int partTotal) { remoteKey+=remoteKeyPart; if(partNum!=partTotal) return; try { finishFirstParty(remoteKey); w.getMessageDisplay().showInfo("Encrypted chat ready."); w.setEnabled(true); gotRemote=true; everGotRemote=true; } catch(GeneralSecurityException e) { context.log("Error confirming remote key",e); w.getMessageDisplay().showError("Remote key could not be confirmed. " + "This is probably because your Java distribution is missing " + "required security features. See system log for details."); } } private void sendCTCP(boolean response,String request,String text) { s.sendLine(IRCMsg.constructBytes((response?"NOTICE " : "PRIVMSG ")+nick+" :\u0001"+ request+" "+text+"\u0001")); } @Override public void doCommand(Commands c,String text) { // Ignore blanks if(text.equals("")) return; boolean action=false; // Check for text or /me if(c.isCommandCharacter(text.charAt(0))) { if(text.startsWith("/me ")) { action=true; text=text.substring(4); } else if(text.startsWith("/say ")) { action=false; text=text.substring(5); } else { // Do other commands normally c.doCommand(text,s,null,null,w.getMessageDisplay(),false); return; } } // Encrypt message String encoded; try { encoded=encode(text); sendCTCP(false,EncryptionPlugin.CTCP_TEXT,(action?"A":"S")+" "+encoded); // Display message w.getMessageDisplay().showOwnText(action ? MessageDisplay.TYPE_ACTION : MessageDisplay.TYPE_MSG,nick,text); } catch(GeneralSecurityException e) { context.log("Encryption error",e); w.getMessageDisplay().showError( "An encryption error occurred. Please try closing and reopening the window."); } } void text(boolean action,String data) { // Decrypt text try { // Show text w.showRemoteText(action ? MessageDisplay.TYPE_ACTION : MessageDisplay.TYPE_MSG,nick,decode(data)); } catch(GeneralSecurityException e) { context.log("Decryption error",e); w.getMessageDisplay().showError( "An encryption error occurred. Please try closing and reopening the window."); } } void end() { w.setEnabled(false); gotRemote=false; w.getMessageDisplay().showInfo("- <nick>"+XML.esc(nick)+"</nick> has closed the session"); } void nick(String newNick) { this.nick=newNick; w.setTarget(nick); w.setTitle(nick+" - Encrypted chat"); } @Override public void windowClosed() { if(gotRemote && s.isConnected()) { sendCTCP(false,EncryptionPlugin.CTCP_END,""); gotRemote=false; } context.unrequestMessages(null,this,PluginContext.ALLREQUESTS); w=null; ((EncryptionPlugin)context.getPlugin()).killSession(s,this); } private String initFirstParty() throws GeneralSecurityException { // Create the parameter generator for a 1024-bit DH key pair AlgorithmParameterGenerator paramGen1=AlgorithmParameterGenerator.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT); paramGen1.init(1024); // Generate the parameters AlgorithmParameters params1=paramGen1.generateParameters(); DHParameterSpec dhSpec1=params1.getParameterSpec(DHParameterSpec.class); // Use the values to generate a key pair KeyPairGenerator keyGen1 = KeyPairGenerator.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT); keyGen1.initialize(dhSpec1); KeyPair keypair1 = keyGen1.generateKeyPair(); // Get the generated public and private keys firstPartyPrivate=keypair1.getPrivate(); PublicKey publicKey1 = keypair1.getPublic(); // Send the public key bytes to the other party... return Base64.encodeBytes(publicKey1.getEncoded(),Base64.DONT_BREAK_LINES); } private void finishFirstParty(String remotePublicKeyBase64) throws GeneralSecurityException { // Convert the public key bytes into a PublicKey object X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec( Base64.decode(remotePublicKeyBase64)); PublicKey remotePublicKey=KeyFactory.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT).generatePublic(x509KeySpec); // Prepare to generate the secret key with the private key and public key of the other party KeyAgreement ka=KeyAgreement.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT); ka.init(firstPartyPrivate); firstPartyPrivate=null; ka.doPhase(remotePublicKey, true); initCiphers(ka); } private void initCiphers(KeyAgreement ka) throws GeneralSecurityException { // Generate the secret key SecretKey key=ka.generateSecret(EncryptionPlugin.ALGORITHM_CIPHER); // Construct the ciphers encrypter = Cipher.getInstance(EncryptionPlugin.ALGORITHM_CIPHER); decrypter = Cipher.getInstance(EncryptionPlugin.ALGORITHM_CIPHER); encrypter.init(Cipher.ENCRYPT_MODE, key); decrypter.init(Cipher.DECRYPT_MODE, key); } private String initSecondParty(String remotePublicKeyBase64) throws GeneralSecurityException { // Convert the public key bytes into a PublicKey object X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec( Base64.decode(remotePublicKeyBase64)); PublicKey remotePublicKey=KeyFactory.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT).generatePublic(x509KeySpec); // Use the parameters from this key to generate a key pair KeyPairGenerator keyGen = KeyPairGenerator.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT); keyGen.initialize(((DHPublicKey)remotePublicKey).getParams()); KeyPair keyPair = keyGen.generateKeyPair(); // Get the generated public and private keys PrivateKey privateKey=keyPair.getPrivate(); PublicKey publicKey=keyPair.getPublic(); // Prepare to generate the secret key with the private key and public key of the other party KeyAgreement ka=KeyAgreement.getInstance(EncryptionPlugin.ALGORITHM_KEYAGREEMENT); ka.init(privateKey); ka.doPhase(remotePublicKey, true); initCiphers(ka); // Public key bytes... return Base64.encodeBytes(publicKey.getEncoded(),Base64.DONT_BREAK_LINES); } private String encode(String data) throws GeneralSecurityException { try { return Base64.encodeBytes(encrypter.doFinal(data.getBytes("UTF-8")),Base64.DONT_BREAK_LINES); } catch(UnsupportedEncodingException e) { throw new BugException(e); } } private String decode(String data) throws GeneralSecurityException { try { return new String(decrypter.doFinal(Base64.decode(data)),"UTF-8"); } catch(UnsupportedEncodingException e) { throw new BugException(e); } } @Override public void internalAction(Element e) { } }