* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program 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 2 of the License, * or (at your option) any later version. * This program 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 this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "AdiumOTREncryption.h" #import <Adium/AIContentMessage.h> #import <Adium/AIAccountControllerProtocol.h> #import <Adium/AIChatControllerProtocol.h> #import <Adium/AIContactControllerProtocol.h> #import <Adium/AIContentControllerProtocol.h> #import <Adium/AIInterfaceControllerProtocol.h> #import <Adium/AILoginControllerProtocol.h> #import <Adium/AIAccount.h> #import <Adium/AIService.h> #import <Adium/AIContentMessage.h> #import <Adium/AIListObject.h> #import <Adium/AIListContact.h> #import "AIHTMLDecoder.h" #import <AIUtilities/AIStringAdditions.h> #import "ESOTRPrivateKeyGenerationWindowController.h" #import "ESOTRPreferences.h" #import "ESOTRUnknownFingerprintController.h" #define PRIVKEY_PATH [[[adium.loginController userDirectory] stringByAppendingPathComponent:@"otr.private_key"] UTF8String] #define STORE_PATH [[[adium.loginController userDirectory] stringByAppendingPathComponent:@"otr.fingerprints"] UTF8String] #define CLOSED_CONNECTION_MESSAGE "has closed his private connection to you" /* OTRL_POLICY_MANUAL doesn't let us respond to other users' automatic attempts at encryption. * If either user has OTR set to Automatic, an OTR session should be begun; without this modified * mask, both users would have to be on automatic for OTR to begin automatically, even though one user * _manually_ attempting OTR will _automatically_ bring the other into OTR even if the setting is Manual. #define OTRL_POLICY_MANUAL_AND_RESPOND_TO_WHITESPACE ( OTRL_POLICY_MANUAL | \ OTRL_POLICY_WHITESPACE_START_AKE | \ OTRL_POLICY_ERROR_START_AKE ) @interface AdiumOTREncryption () - ( void ) prepareEncryption ; - ( void ) setSecurityDetails: ( NSDictionary * ) securityDetailsDict forChat: ( AIChat * ) inChat ; - ( NSString * ) localizedOTRMessage: ( NSString * ) message withUsername: ( NSString * ) username isWorthOpeningANewChat: ( BOOL * ) isWorthOpeningANewChat ; - ( void ) notifyWithTitle: ( NSString * ) title primary: ( NSString * ) primary secondary: ( NSString * ) secondary ; - ( void ) upgradeOTRIfNeeded ; - ( void ) adiumFinishedLaunching: ( NSNotification * ) inNotification ; - ( void ) adiumWillTerminate: ( NSNotification * ) inNotification ; - ( void ) updateSecurityDetails: ( NSNotification * ) inNotification ; - ( void ) verifyUnknownFingerprint: ( NSValue * ) contextValue ; @implementation AdiumOTREncryption /* We'll only use the one OtrlUserState. */ static OtrlUserState otrg_plugin_userstate = NULL ; static AdiumOTREncryption * adiumOTREncryption = nil ; void otrg_ui_update_fingerprint ( void ); void update_security_details_for_chat ( AIChat * chat ); void send_default_query_to_chat ( AIChat * inChat ); void disconnect_from_chat ( AIChat * inChat ); void disconnect_from_context ( ConnContext * context ); TrustLevel otrg_plugin_context_to_trust ( ConnContext * context ); if ( adiumOTREncryption ) { return [ adiumOTREncryption retain ]; if (( self = [ super init ])) { adiumOTREncryption = self ; //Wait for Adium to finish launching to prepare encryption so that accounts will be loaded [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( adiumFinishedLaunching : ) name : AIApplicationDidFinishLoadingNotification gaim_signal_connect(conn_handle, "signed-on", otrg_plugin_handle, GAIM_CALLBACK(process_connection_change), NULL); gaim_signal_connect(conn_handle, "signed-off", otrg_plugin_handle, GAIM_CALLBACK(process_connection_change), NULL); - ( void ) adiumFinishedLaunching: ( NSNotification * ) inNotification [ self prepareEncryption ]; - ( void ) prepareEncryption /* Initialize the OTR library */ [ self upgradeOTRIfNeeded ]; /* Make our OtrlUserState; we'll only use the one. */ otrg_plugin_userstate = otrl_userstate_create (); err = otrl_privkey_read ( otrg_plugin_userstate , PRIVKEY_PATH ); const char * errMsg = gpg_strerror ( err ); if ( errMsg && strcmp ( errMsg , "No such file or directory" )) { NSLog ( @"Error reading %s: %s" , PRIVKEY_PATH , errMsg ); otrg_ui_update_keylist (); err = otrl_privkey_read_fingerprints ( otrg_plugin_userstate , STORE_PATH , const char * errMsg = gpg_strerror ( err ); if ( errMsg && strcmp ( errMsg , "No such file or directory" )) { NSLog ( @"Error reading %s: %s" , STORE_PATH , errMsg ); otrg_ui_update_fingerprint (); [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( adiumWillTerminate : ) name : AIAppWillTerminateNotification [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( updateSecurityDetails : ) [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( updateSecurityDetails : ) name : Chat_DestinationChanged [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( updateSecurityDetails : ) //Add the Encryption preferences OTRPrefs = [( ESOTRPreferences * )[ ESOTRPreferences preferencePane ] retain ]; [[ NSNotificationCenter defaultCenter ] removeObserver : self ]; * @brief Return an NSDictionary* describing a ConnContext. * @"Their Fingerprint" : NSString of the contact's fingerprint's human-readable hash * @"Our Fingerprint" : NSString of our fingerprint's human-readable hash * @"Incoming SessionID" : NSString of the incoming sessionID * @"Outgoing SessionID" : NSString of the outgoing sessionID * @"EncryptionStatus" : An AIEncryptionStatus * @"AIAccount" : The AIAccount of this context * @"who" : The UID of the remote user * static NSDictionary * details_for_context ( ConnContext * context ) if ( ! context ) return nil ; NSDictionary * securityDetailsDict ; Fingerprint * fprint = context -> active_fingerprint ; if ( ! fprint || ! ( fprint -> fingerprint )) return nil ; context = fprint -> context ; if ( ! context ) return nil ; TrustLevel level = otrg_plugin_context_to_trust ( context ); AIEncryptionStatus encryptionStatus ; encryptionStatus = EncryptionStatus_None ; encryptionStatus = EncryptionStatus_Unverified ; encryptionStatus = EncryptionStatus_Verified ; encryptionStatus = EncryptionStatus_Finished ; char our_hash [ 45 ], their_hash [ 45 ]; otrl_privkey_fingerprint ( otrg_get_userstate (), our_hash , context -> accountname , context -> protocol ); otrl_privkey_hash_to_human ( their_hash , fprint -> fingerprint ); unsigned char * sessionid ; char sess1 [ 21 ], sess2 [ 21 ]; BOOL sess1_outgoing = ( context -> sessionid_half == OTRL_SESSIONID_FIRST_HALF_BOLD ); size_t idhalflen = ( context -> sessionid_len ) / 2 ; /* Make a human-readable version of the sessionid (in two parts) */ sessionid = context -> sessionid ; for ( NSUInteger i = 0 ; i < idhalflen ; ++ i ) sprintf ( sess1 + ( 2 * i ), "%02x" , sessionid [ i ]); for ( NSUInteger i = 0 ; i < idhalflen ; ++ i ) sprintf ( sess2 + ( 2 * i ), "%02x" , sessionid [ i + idhalflen ]); account = [ adium . accountController accountWithInternalObjectID : [ NSString stringWithUTF8String : context -> accountname ]]; securityDetailsDict = [ NSDictionary dictionaryWithObjectsAndKeys : [ NSString stringWithUTF8String : their_hash ], @"Their Fingerprint" , [ NSString stringWithUTF8String : our_hash ], @"Our Fingerprint" , [ NSNumber numberWithInteger : encryptionStatus ], @"EncryptionStatus" , [ NSString stringWithUTF8String : context -> username ], @"who" , [ NSString stringWithUTF8String : sess1 ], ( sess1_outgoing ? @"Outgoing SessionID" : @"Incoming SessionID" ), [ NSString stringWithUTF8String : sess2 ], ( sess1_outgoing ? @"Incoming SessionID" : @"Outgoing SessionID" ), AILog ( @"Security details: %@" , securityDetailsDict ); return securityDetailsDict ; static AIAccount * accountFromAccountID ( const char * accountID ) return [ adium . accountController accountWithInternalObjectID : [ NSString stringWithUTF8String : accountID ]]; static AIService * serviceFromServiceID ( const char * serviceID ) return [ adium . accountController serviceWithUniqueID : [ NSString stringWithUTF8String : serviceID ]]; static AIListContact * contactFromInfo ( const char * accountID , const char * serviceID , const char * username ) return [ adium . contactController contactWithService : serviceFromServiceID ( serviceID ) account : accountFromAccountID ( accountID ) UID :[ NSString stringWithUTF8String : username ]]; static AIListContact * contactForContext ( ConnContext * context ) return contactFromInfo ( context -> accountname , context -> protocol , context -> username ); static AIChat * chatForContext ( ConnContext * context ) AIListContact * listContact = contactForContext ( context ); AIChat * chat = [ adium . chatController existingChatWithContact : listContact ]; chat = [ adium . chatController chatWithContact : listContact ]; static OtrlPolicy policyForContact ( AIListContact * contact ) OtrlPolicy policy = OTRL_POLICY_MANUAL_AND_RESPOND_TO_WHITESPACE ; //Force OTRL_POLICY_MANUAL when interacting with mobile numbers if ([ contact . UID hasPrefix : @"+" ]) { policy = OTRL_POLICY_MANUAL_AND_RESPOND_TO_WHITESPACE ; AIEncryptedChatPreference pref = contact . encryptedChatPreferences ; case EncryptedChat_Never : policy = OTRL_POLICY_NEVER ; case EncryptedChat_Manually : case EncryptedChat_Default : policy = OTRL_POLICY_MANUAL_AND_RESPOND_TO_WHITESPACE ; case EncryptedChat_Automatically : policy = OTRL_POLICY_OPPORTUNISTIC ; case EncryptedChat_RejectUnencryptedMessages : policy = OTRL_POLICY_ALWAYS ; //Return the ConnContext for a Conversation, or NULL if none exists static ConnContext * contextForChat ( AIChat * chat ) const char * username , * accountname , * proto ; /* Do nothing if this isn't an IM conversation */ if ( chat . isGroupChat ) return nil ; accountname = [ account . internalObjectID UTF8String ]; proto = [ account . service . serviceCodeUniqueID UTF8String ]; username = [ chat . listObject . UID UTF8String ]; context = otrl_context_find ( otrg_plugin_userstate , username , accountname , proto , 0 , NULL , /* What level of trust do we have in the privacy of this ConnContext? */ TrustLevel otrg_plugin_context_to_trust ( ConnContext * context ) TrustLevel level = TRUST_NOT_PRIVATE ; if ( context && context -> msgstate == OTRL_MSGSTATE_ENCRYPTED ) { if ( context -> active_fingerprint -> trust && context -> active_fingerprint -> trust [ 0 ] != '\0' ) { level = TRUST_UNVERIFIED ; } else if ( context && context -> msgstate == OTRL_MSGSTATE_FINISHED ) { /* Return the OTR policy for the given context. */ static OtrlPolicy policy_cb ( void * opdata , ConnContext * context ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; OtrlPolicy ret = policyForContact ( contactForContext ( context )); /* Generate a private key for the given accountname/protocol */ void otrg_plugin_create_privkey ( const char * accountname , AIAccount * account = accountFromAccountID ( accountname ); AIService * service = serviceFromServiceID ( protocol ); NSString * identifier = [ NSString stringWithFormat : @"%@ (%@)" , account . formattedUID , [ service shortDescription ]]; [ ESOTRPrivateKeyGenerationWindowController startedGeneratingForIdentifier : identifier ]; otrl_privkey_generate ( otrg_plugin_userstate , PRIVKEY_PATH , otrg_ui_update_keylist (); /* Mark the dialog as done. */ [ ESOTRPrivateKeyGenerationWindowController finishedGeneratingForIdentifier : identifier ]; /* Create a private key for the given accountname/protocol if static void create_privkey_cb ( void * opdata , const char * accountname , NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; otrg_plugin_create_privkey ( accountname , protocol ); /* Report whether you think the given user is online. Return 1 if * you think he is, 0 if you think he isn't, -1 if you're not sure. * If you return 1, messages such as heartbeats or other * notifications may be sent to the user, which could result in "not * logged in" errors if you're wrong. */ static int is_logged_in_cb ( void * opdata , const char * accountname , const char * protocol , const char * recipient ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; AIListContact * contact = contactFromInfo ( accountname , protocol , recipient ); if ([ contact statusSummary ] == AIUnknownStatus ) ret = ( contact . online ? 1 : 0 ); /* Send the given IM to the given recipient from the given * accountname/protocol. */ static void inject_message_cb ( void * opdata , const char * accountname , const char * protocol , const char * recipient , const char * message ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; [ adium . contentController sendRawMessage : [ NSString stringWithUTF8String : message ] toContact : contactFromInfo ( accountname , protocol , recipient )]; * @brief Display an OTR message * This should be displayed within the relevant chat. * @result 0 if we handled displaying the message; 1 if we could not static int display_otr_message ( const char * accountname , const char * protocol , const char * username , const char * msg ) AIListContact * listContact = contactFromInfo ( accountname , protocol , username ); AIContentMessage * messageObject ; //We couldn't determine a listContact, so return that we didn't handle the message if ( ! listContact ) return 1 ; chat = [ adium . chatController existingChatWithContact : listContact ]; message = [ NSString stringWithUTF8String : msg ]; AILog ( @"display_otr_message: %s %s %s: %s" , accountname , protocol , username , msg ); if (([ message rangeOfString : @"<b>The following message received from" ]. location != NSNotFound ) && ([ message rangeOfString : @"was <i>not</i> encrypted: [" ]. location != NSNotFound )) { * If we receive an unencrypted message, display it as a normal incoming message with the bolded warning that * the message was not encrypted NSRange endRange = [ message rangeOfString : @"was <i>not</i> encrypted: [" ]; /* The message will be formatted as: * <b>The following message received from tekjew was <i>not</i> encrypted: [</b>MESSAGE_HERE - POTENTIALLY HTML<b>]</b> NSString * OTRMessage = [ adiumOTREncryption localizedOTRMessage : @"The following message was <b>not encrypted</b>: " isWorthOpeningANewChat : NULL ]; message = [ OTRMessage stringByAppendingString : [ message substringWithRange : NSMakeRange ( NSMaxRange ( endRange ), ([ message length ] - NSMaxRange ( endRange ) - [ @"<b>]</b>" length ]))]]; //Create a new chat if necessary if ( ! chat ) chat = [ adium . chatController chatWithContact : listContact ]; messageObject = [ AIContentMessage messageInChat : chat message :[ AIHTMLDecoder decodeHTML : message ] [ adium . contentController receiveContentObject : messageObject ]; BOOL isWorthOpeningANewChat = NO ; //All other OTR messages should be displayed as status messages; decode the message to strip any HTML message = [ adiumOTREncryption localizedOTRMessage : message withUsername : listContact . displayName isWorthOpeningANewChat : & isWorthOpeningANewChat ]; if ( isWorthOpeningANewChat ) { //Create a new chat if we don't already have one and this message is worth it chat = [ adium . chatController chatWithContact : listContact ]; /* It's not worth opening a new chat. If we found a chat but it's not open, which can happen if the chat is still * being used by some delayed process, don't display a message thereby opening it. if ( ! [ chat isOpen ]) chat = nil ; [ adium . contentController displayEvent : [[ AIHTMLDecoder decodeHTML : message ] string ] /* Display a notification message for a particular accountname / * protocol / username conversation. */ static void notify_cb ( void * opdata , OtrlNotifyLevel level , const char * accountname , const char * protocol , const char * username , const char * title , const char * primary , const char * secondary ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; AIListContact * listContact = contactFromInfo ( accountname , protocol , username ); NSString * displayName = listContact . displayName ; [ adiumOTREncryption notifyWithTitle : [ adiumOTREncryption localizedOTRMessage : [ NSString stringWithUTF8String : title ] isWorthOpeningANewChat : NULL ] primary :[ adiumOTREncryption localizedOTRMessage : [ NSString stringWithUTF8String : primary ] isWorthOpeningANewChat : NULL ] secondary :[ adiumOTREncryption localizedOTRMessage : [ NSString stringWithUTF8String : secondary ] isWorthOpeningANewChat : NULL ]]; /* Display an OTR control message for a particular accountname / * protocol / username conversation. Return 0 if you are able to * successfully display it. If you return non-0 (or if this * function is NULL), the control message will be displayed inline, * as a received message, or else by using the above notify() static int display_otr_message_cb ( void * opdata , const char * accountname , const char * protocol , const char * username , const char * msg ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; int ret = display_otr_message ( accountname , protocol , username , msg ); /* When the list of ConnContexts changes (including a change in * state), this is called so the UI can be updated. */ static void update_context_list_cb ( void * opdata ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; otrg_ui_update_keylist (); /* Return a newly allocated string containing a human-friendly * representation for the given account */ static const char * account_display_name_cb ( void * opdata , const char * accountname , const char * protocol ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; const char * ret = strdup ([[ accountFromAccountID ( accountname ) formattedUID ] UTF8String ]); /* Deallocate a string returned by account_name */ static void account_display_name_free_cb ( void * opdata , const char * account_display_name ) if ( account_display_name ) free (( char * ) account_display_name ); /* Return a newly allocated string containing a human-friendly name * for the given protocol id */ static const char * protocol_name_cb ( void * opdata , const char * protocol ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; const char * ret = strdup ([[ serviceFromServiceID ( protocol ) shortDescription ] UTF8String ]); /* Deallocate a string allocated by protocol_name */ static void protocol_name_free_cb ( void * opdata , const char * protocol_name ) free (( char * ) protocol_name ); /* A new fingerprint for the given user has been received. */ static void new_fingerprint_cb ( void * opdata , OtrlUserState us , const char * accountname , const char * protocol , const char * username , unsigned char fingerprint [ 20 ]) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; context = otrl_context_find ( us , username , accountname , protocol , 0 , NULL , NULL , NULL ); if ( context == NULL /* || context->msgstate != OTRL_MSGSTATE_ENCRYPTED*/ ) { NSLog ( @"otrg_adium_dialog_unknown_fingerprint: Ack!" ); [ adiumOTREncryption performSelector : @selector ( verifyUnknownFingerprint : ) withObject :[ NSValue valueWithPointer : context ] /* The list of known fingerprints has changed. Write them to disk. */ static void write_fingerprints_cb ( void * opdata ) otrg_plugin_write_fingerprints (); /* A ConnContext has entered a secure state. */ static void gone_secure_cb ( void * opdata , ConnContext * context ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; AIChat * chat = chatForContext ( context ); update_security_details_for_chat ( chat ); otrg_ui_update_fingerprint (); /* A ConnContext has left a secure state. */ static void gone_insecure_cb ( void * opdata , ConnContext * context ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; AIChat * chat = chatForContext ( context ); update_security_details_for_chat ( chat ); otrg_ui_update_fingerprint (); /* We have completed an authentication, using the D-H keys we * already knew. is_reply indicates whether we initiated the AKE. */ static void still_secure_cb ( void * opdata , ConnContext * context , int is_reply ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; // otrg_dialog_stillconnected(context); AILog ( @"Still secure..." ); /* Log a message. The passed message will end in "\n". */ static void log_message_cb ( void * opdata , const char * message ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; AILog ( @"otr: %s" , ( message ? message : "(null)" )); * @brief Find the maximum message size supported by this protocol. * This method is called whenever a message is about to be sent with * fragmentation enabled. The return value is checked against the size of * the message to be sent to determine whether fragmentation is necessary. * Setting max_message_size to NULL will disable the fragmentation of all * sent messages; returning 0 from this callback will disable fragmentation * of a particular message. The latter is useful, for example, for * protocols like XMPP (Jabber) that do not require fragmentation at all. int max_message_size_cb ( void * opdata , ConnContext * context ) NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; AIChat * chat = chatForContext ( context ); /* Values from https://otr.cypherpunks.ca/UPGRADING-libotr-3.1.0.txt */ static NSDictionary * maxSizeByServiceClassDict = nil ; if ( ! maxSizeByServiceClassDict ) { maxSizeByServiceClassDict = [[ NSDictionary alloc ] initWithObjectsAndKeys : [ NSNumber numberWithInteger : 2343 ], @"AIM-compatible" , [ NSNumber numberWithInteger : 1409 ], @"MSN" , [ NSNumber numberWithInteger : 832 ], @"Yahoo!" , [ NSNumber numberWithInteger : 1999 ], @"Gadu-Gadu" , [ NSNumber numberWithInteger : 417 ], @"IRC" , /* This will return 0 if we don't know (unknown protocol) or don't need it (Jabber), * which will disable fragmentation. int ret = [[ maxSizeByServiceClassDict objectForKey : chat . account . service . serviceClass ] intValue ]; static OtrlMessageAppOps ui_ops = { account_display_name_free_cb , - ( void ) willSendContentMessage: ( AIContentMessage * ) inContentMessage const char * originalMessage = [[ inContentMessage encodedMessage ] UTF8String ]; AIAccount * account = ( AIAccount * )[ inContentMessage source ]; const char * accountname = [ account . internalObjectID UTF8String ]; const char * protocol = [ account . service . serviceCodeUniqueID UTF8String ]; const char * username = [[[ inContentMessage destination ] UID ] UTF8String ]; char * fullOutgoingMessage = NULL ; if ( ! username || ! originalMessage ) err = otrl_message_sending ( otrg_plugin_userstate , & ui_ops , /* opData */ NULL , accountname , protocol , username , originalMessage , /* tlvs */ NULL , & fullOutgoingMessage , /* add_appdata cb */ NULL , /* appdata */ NULL ); if ( err && fullOutgoingMessage == NULL ) { //Be *sure* not to send out plaintext [ inContentMessage setEncodedMessage : nil ]; } else if ( fullOutgoingMessage ) { /* We got a message to send. Fragment it, saving the last fragment so Adium has something to do (and therefore * knows that a message is really being sent. char * lastFragmentOfMessage = NULL ; ConnContext * context = contextForChat ( inContentMessage . chat ); otrl_message_fragment_and_send ( & ui_ops , /* opData */ NULL , context , fullOutgoingMessage , OTRL_FRAGMENT_SEND_ALL_BUT_LAST , & lastFragmentOfMessage ); //This new message is what should be sent to the remote contact [ inContentMessage setEncodedMessage : [ NSString stringWithUTF8String : lastFragmentOfMessage ]]; //We're now done with the messages allocated by OTR otrl_message_free ( fullOutgoingMessage ); otrl_message_free ( lastFragmentOfMessage ); /* Abort the SMP protocol. Used when malformed or unexpected messages static void otrg_plugin_abort_smp ( ConnContext * context ) otrl_message_abort_smp ( otrg_plugin_userstate , & ui_ops , NULL , context ); /* Start the Socialist Millionaires' Protocol over the current connection, * using the given initial secret. */ void otrg_plugin_start_smp ( ConnContext * context , const unsigned char * secret , size_t secretlen ) otrl_message_initiate_smp ( otrg_plugin_userstate , & ui_ops , NULL , context , secret , secretlen ); /* Continue the Socialist Millionaires' Protocol over the current connection, * using the given initial secret (ie finish step 2). */ void otrg_plugin_continue_smp ( ConnContext * context , const unsigned char * secret , size_t secretlen ) otrl_message_respond_smp ( otrg_plugin_userstate , & ui_ops , NULL , context , secret , secretlen ); /* Show a dialog asking the user to respond to an SMP secret sent by a remote contact. * Our user should enter the same secret entered by the remote contact. */ static void otrg_dialogue_respond_socialist_millionaires ( ConnContext * context ) if ( context == NULL || context -> msgstate != OTRL_MSGSTATE_ENCRYPTED ) /* XXX Implement me - prompt to respond to a secret, and then call * otrg_plugin_continue_smp() with the secret and the appropriate context */ static void otrg_dialog_update_smp ( ConnContext * context , CGFloat percentage ) - ( NSString * ) decryptIncomingMessage: ( NSString * ) inString fromContact: ( AIListContact * ) inListContact onAccount: ( AIAccount * ) inAccount NSString * decryptedMessage = nil ; const char * message = [ inString UTF8String ]; const char * username = [ inListContact . UID UTF8String ]; const char * accountname = [ inAccount . internalObjectID UTF8String ]; const char * protocol = [ inAccount . service . serviceCodeUniqueID UTF8String ]; /* If newMessage is set to non-NULL and res is 0, use newMessage. * If newMessage is set to non-NULL and res is not 0, display nothing as this was an OTR message * If newMessage is set to NULL and res is 0, use message res = otrl_message_receiving ( otrg_plugin_userstate , & ui_ops , NULL , accountname , protocol , username , message , & newMessage , & tlvs , NULL , NULL ); if ( ! newMessage && ! res ) { //Use the original mesage; this was not an OTR-related message decryptedMessage = inString ; AILogWithSignature ( @"Not OTR-related message for decryption." ); } else if ( newMessage && ! res ) { //We decryped an OTR-encrypted message decryptedMessage = [ NSString stringWithUTF8String : newMessage ]; AILogWithSignature ( @"Decrypted an OTR message." ); } else /* (newMessage && res) */ { //This was an OTR protocol message AILogWithSignature ( @"Skipping an OTR protocol message." ); otrl_message_free ( newMessage ); tlv = otrl_tlv_find ( tlvs , OTRL_TLV_DISCONNECTED ); /* Notify the user that the other side disconnected. */ display_otr_message ( accountname , protocol , username , CLOSED_CONNECTION_MESSAGE ); otrg_ui_update_keylist (); /* Keep track of our current progress in the Socialist Millionaires' ConnContext * context = otrl_context_find ( otrg_plugin_userstate , username , accountname , protocol , 0 , NULL , NULL , NULL ); NextExpectedSMP nextMsg = context -> smstate -> nextExpected ; tlv = otrl_tlv_find ( tlvs , OTRL_TLV_SMP1 ); if ( nextMsg != OTRL_SMP_EXPECT1 ) otrg_plugin_abort_smp ( context ); otrg_dialogue_respond_socialist_millionaires ( context ); tlv = otrl_tlv_find ( tlvs , OTRL_TLV_SMP2 ); if ( nextMsg != OTRL_SMP_EXPECT2 ) otrg_plugin_abort_smp ( context ); otrg_dialog_update_smp ( context , 0.6f ); context -> smstate -> nextExpected = OTRL_SMP_EXPECT4 ; tlv = otrl_tlv_find ( tlvs , OTRL_TLV_SMP3 ); if ( nextMsg != OTRL_SMP_EXPECT3 ) otrg_plugin_abort_smp ( context ); otrg_dialog_update_smp ( context , 1.0f ); context -> smstate -> nextExpected = OTRL_SMP_EXPECT1 ; tlv = otrl_tlv_find ( tlvs , OTRL_TLV_SMP4 ); if ( nextMsg != OTRL_SMP_EXPECT4 ) otrg_plugin_abort_smp ( context ); otrg_dialog_update_smp ( context , 1.0f ); context -> smstate -> nextExpected = OTRL_SMP_EXPECT1 ; tlv = otrl_tlv_find ( tlvs , OTRL_TLV_SMP_ABORT ); otrg_dialog_update_smp ( context , 0.0f ); context -> smstate -> nextExpected = OTRL_SMP_EXPECT1 ; - ( void ) requestSecureOTRMessaging: ( BOOL ) inSecureMessaging inChat: ( AIChat * ) inChat send_default_query_to_chat ( inChat ); disconnect_from_chat ( inChat ); - ( void ) promptToVerifyEncryptionIdentityInChat: ( AIChat * ) inChat ConnContext * context = contextForChat ( inChat ); NSDictionary * responseInfo = details_for_context ( context );; [ ESOTRUnknownFingerprintController showVerifyFingerprintPromptWithResponseInfo : responseInfo ]; * @brief Adium will begin terminating * Send the OTRL_TLV_DISCONNECTED packets when we're about to quit before we disconnect - ( void ) adiumWillTerminate: ( NSNotification * ) inNotification ConnContext * context = otrg_plugin_userstate -> context_root ; ConnContext * next = context -> next ; if ( context -> msgstate == OTRL_MSGSTATE_ENCRYPTED && context -> protocol_version > 1 ) { disconnect_from_context ( context ); * @brief A chat notification was posted after which we should update our security details * @param inNotification A notification whose object is the AIChat in question - ( void ) updateSecurityDetails: ( NSNotification * ) inNotification AILog ( @"Updating security details for %@" ,[ inNotification object ]); update_security_details_for_chat ([ inNotification object ]); void update_security_details_for_chat ( AIChat * inChat ) ConnContext * context = contextForChat ( inChat ); [ adiumOTREncryption setSecurityDetails : details_for_context ( context ) - ( void ) setSecurityDetails: ( NSDictionary * ) securityDetailsDict forChat: ( AIChat * ) inChat NSMutableDictionary * fullSecurityDetailsDict ; if ( securityDetailsDict ) { NSString * format , * description ; fullSecurityDetailsDict = [[ securityDetailsDict mutableCopy ] autorelease ]; /* Encrypted by Off-the-Record Messaging * Fingerprint for TekJew: * Secure ID for this session: * Incoming: <Incoming SessionID> * Outgoing: <Outgoing SessionID> format = [ @"%@ \n\n " stringByAppendingString : AILocalizedString ( @"Fingerprint for %@:" , "Fingerprint for <name>:" )]; format = [ format stringByAppendingString : @" \n %@ \n\n %@ \n %@ %@ \n %@ %@" ]; description = [ NSString stringWithFormat : format , AILocalizedString ( @"Encrypted by Off-the-Record Messaging" , nil ), [[ inChat listObject ] formattedUID ], [ securityDetailsDict objectForKey : @"Their Fingerprint" ], AILocalizedString ( @"Secure ID for this session:" , nil ), AILocalizedString ( @"Incoming:" , "This is shown before the Off-the-Record Session ID (a series of numbers and letters) sent by the other party with whom you are having an encrypted chat." ), [ securityDetailsDict objectForKey : @"Incoming SessionID" ], AILocalizedString ( @"Outgoing:" , "This is shown before the Off-the-Record Session ID (a series of numbers and letters) sent by you to the other party with whom you are having an encrypted chat." ), [ securityDetailsDict objectForKey : @"Outgoing SessionID" ], [ fullSecurityDetailsDict setObject : description fullSecurityDetailsDict = nil ; [ inChat setSecurityDetails : fullSecurityDetailsDict ]; void send_default_query_to_chat ( AIChat * inChat ) //Note that we pass a name for display, not internal usage char * msg = otrl_proto_default_query_msg ([ inChat . account . formattedUID UTF8String ], policyForContact ([ inChat listObject ])); [ adium . contentController sendRawMessage : [ NSString stringWithUTF8String : ( msg ? msg : "?OTRv2?" )] toContact :[ inChat listObject ]]; /* Disconnect a context, sending a notice to the other side, if void disconnect_from_context ( ConnContext * context ) otrl_message_disconnect ( otrg_plugin_userstate , & ui_ops , NULL , context -> accountname , context -> protocol , context -> username ); gone_insecure_cb ( NULL , context ); void disconnect_from_chat ( AIChat * inChat ) disconnect_from_context ( contextForChat ( inChat )); /* Forget a fingerprint */ void otrg_ui_forget_fingerprint ( Fingerprint * fingerprint ) /* Don't do anything with the active fingerprint if we're in the context = ( fingerprint ? fingerprint -> context : NULL ); if ( context && ( context -> msgstate == OTRL_MSGSTATE_ENCRYPTED && context -> active_fingerprint == fingerprint )) return ; otrl_context_forget_fingerprint ( fingerprint , 1 ); otrg_plugin_write_fingerprints (); void otrg_plugin_write_fingerprints ( void ) otrl_privkey_write_fingerprints ( otrg_plugin_userstate , STORE_PATH ); otrg_ui_update_fingerprint (); void otrg_ui_update_keylist ( void ) [ adiumOTREncryption prefsShouldUpdatePrivateKeyList ]; void otrg_ui_update_fingerprint ( void ) [ adiumOTREncryption prefsShouldUpdateFingerprintsList ]; OtrlUserState otrg_get_userstate ( void ) return otrg_plugin_userstate ; - ( void ) verifyUnknownFingerprint: ( NSValue * ) contextValue NSDictionary * responseInfo ; responseInfo = details_for_context ([ contextValue pointerValue ]); [ ESOTRUnknownFingerprintController showUnknownFingerprintPromptWithResponseInfo : responseInfo ]; /* This means either context, context->active_fingerprint, context->active_fingerprint-fingerprint * or context->active_fingerprint->context was NULL. AILogWithSignature ( @"Got a nil details_for_context for %p" , [ contextValue pointerValue ]); * @brief Call this function when our DSA key is updated; it will redraw the Encryption preferences item, if visible. - ( void ) prefsShouldUpdatePrivateKeyList [ OTRPrefs updatePrivateKeyList ]; * @brief Update the list of other users' fingerprints, if it's visible - ( void ) prefsShouldUpdateFingerprintsList [ OTRPrefs updateFingerprintsList ]; #pragma mark Localization * @brief Given an English message from libotr, construct a localized version * @param message The original message, which was sent by libotr in English * @param username A username (screenname) for substitution purposes as appropriate. May be nil. * @param isWorthOpeningANewChat On return, YES if display of this message should open a chat if one doesn't exist. Pass NULL if you don't care. - ( NSString * ) localizedOTRMessage: ( NSString * ) message withUsername: ( NSString * ) username isWorthOpeningANewChat: ( BOOL * ) isWorthOpeningANewChat NSString * localizedOTRMessage = nil ; if ( isWorthOpeningANewChat ) * isWorthOpeningANewChat = NO ; if (([ message rangeOfString : @"You sent unencrypted data to" ]. location != NSNotFound ) && ([ message rangeOfString : @"who wasn't expecting it" ]. location != NSNotFound )) { localizedOTRMessage = [ NSString stringWithFormat : AILocalizedString ( @"You sent an unencrypted message, but %@ was expecting encryption." , "Message when sending unencrypted messages to a contact expecting encrypted ones. %s will be a name." ), } else if (([ message rangeOfString : @"You sent encrypted data to" ]. location != NSNotFound ) && ([ message rangeOfString : @"who wasn't expecting it" ]. location != NSNotFound )) { localizedOTRMessage = [ NSString stringWithFormat : AILocalizedString ( @"You sent an encrypted message, but %@ was not expecting encryption." , "Message when sending encrypted messages to a contact expecting unencrypted ones. %s will be a name." ), if ( isWorthOpeningANewChat ) * isWorthOpeningANewChat = YES ; } else if ([ message rangeOfString : @ CLOSED_CONNECTION_MESSAGE ]. location != NSNotFound ) { localizedOTRMessage = [ NSString stringWithFormat : AILocalizedString ( @"%@ is no longer using encryption; you should cancel encryption on your side." , "Message when the remote contact cancels his half of an encrypted conversation. %s will be a name." ), } else if ([ message isEqualToString : @"Private connection closed" ]) { localizedOTRMessage = AILocalizedString ( @"Private connection closed" , nil ); } else if ([ message rangeOfString : @"has already closed his private connection to you" ]. location != NSNotFound ) { localizedOTRMessage = [ NSString stringWithFormat : AILocalizedString ( @"%@'s private connection to you is closed." , "Statement that someone's private (encrypted) connection is closed." ), } else if ([ message isEqualToString : @"Your message was not sent. Either close your private connection to him, or refresh it." ]) { localizedOTRMessage = AILocalizedString ( @"Your message was not sent. You should end the encrypted chat on your side or re-request encryption." , nil ); if ( isWorthOpeningANewChat ) * isWorthOpeningANewChat = YES ; } else if ([ message isEqualToString : @"The following message was <b>not encrypted</b>: " ]) { localizedOTRMessage = AILocalizedString ( @"The following message was <b>not encrypted</b>: " , nil ); if ( isWorthOpeningANewChat ) * isWorthOpeningANewChat = YES ; } else if ([ message rangeOfString : @"received an unreadable encrypted" ]. location != NSNotFound ) { localizedOTRMessage = [ NSString stringWithFormat : AILocalizedString ( @"An encrypted message from %@ could not be decrypted." , nil ), if ( isWorthOpeningANewChat ) * isWorthOpeningANewChat = YES ; return ( localizedOTRMessage ? localizedOTRMessage : message ); * @brief Display a message (independent of a chat) * @param title The window title * @param primary The main information for the message * @param secondary Additional information for the message - ( void ) notifyWithTitle: ( NSString * ) title primary: ( NSString * ) primary secondary: ( NSString * ) secondary //XXX todo: search on ops->notify in message.c in libotr and handle / localize the error messages [ adium . interfaceController handleMessage : primary withDescription : secondary #pragma mark Upgrading gaim-otr --> Adium-otr * @brief Construct a dictionary converting libpurple prpl names to Adium serviceIDs for the purpose of fingerprint upgrading - ( NSDictionary * ) prplDict return [ NSDictionary dictionaryWithObjectsAndKeys : @"libpurple-OSCAR-AIM" , @"prpl-oscar" , @"libpurple-Gadu-Gadu" , @"prpl-gg" , @"libpurple-Jabber" , @"prpl-jabber" , @"libpurple-Sametime" , @"prpl-meanwhile" , @"libpurple-MSN" , @"prpl-msn" , @"libpurple-GroupWise" , @"prpl-novell" , @"libpurple-Yahoo!" , @"prpl-yahoo" , @"libpurple-zephyr" , @"prpl-zephyr" , nil ]; - ( NSString * ) upgradedFingerprintsFromFile: ( NSString * ) inPath NSString * sourceFingerprints = [ NSString stringWithContentsOfUTF8File : inPath ]; if ( ! sourceFingerprints || ! [ sourceFingerprints length ]) return nil ; NSScanner * scanner = [ NSScanner scannerWithString : sourceFingerprints ]; NSMutableString * outFingerprints = [ NSMutableString string ]; NSCharacterSet * tabAndNewlineSet = [ NSCharacterSet characterSetWithCharactersInString : @" \t\n\r " ]; [ scanner setCharactersToBeSkipped : [ NSCharacterSet characterSetWithCharactersInString : @" \" " ]]; NSDictionary * prplDict = [ self prplDict ]; while ( ! [ scanner isAtEnd ]) { //username accountname protocol key trusted\n NSString * username = nil , * accountname = nil , * protocol = nil , * key = nil , * trusted = nil ; [ scanner scanUpToCharactersFromSet : tabAndNewlineSet intoString :& username ]; [ scanner scanCharactersFromSet : tabAndNewlineSet intoString : NULL ]; [ scanner scanUpToCharactersFromSet : tabAndNewlineSet intoString :& accountname ]; [ scanner scanCharactersFromSet : tabAndNewlineSet intoString : NULL ]; [ scanner scanUpToCharactersFromSet : tabAndNewlineSet intoString :& protocol ]; [ scanner scanCharactersFromSet : tabAndNewlineSet intoString : NULL ]; [ scanner scanUpToCharactersFromSet : tabAndNewlineSet intoString :& key ]; [ scanner scanCharactersFromSet : tabAndNewlineSet intoString :& chunk ]; //We have a trusted entry if ([ chunk isEqualToString : @" \t " ]) { [ scanner scanUpToCharactersFromSet : tabAndNewlineSet intoString :& trusted ]; [ scanner scanCharactersFromSet : tabAndNewlineSet intoString : NULL ]; if ( username && accountname && protocol && key ) { for ( AIAccount * account in adium . accountController . accounts ) { //Hit every possibile name for this account along the way if ([[ NSSet setWithObjects : account . UID , account . formattedUID ,[ account . UID compactedString ], nil ] containsObject : accountname ]) { if ([ account . service . serviceCodeUniqueID isEqualToString : [ prplDict objectForKey : protocol ]]) { [ outFingerprints appendString : [ NSString stringWithFormat : @"%@ \t %@ \t %@ \t %@" , username , account . internalObjectID , account . service . serviceCodeUniqueID , key ]]; [ outFingerprints appendString : @" \t " ]; [ outFingerprints appendString : trusted ]; [ outFingerprints appendString : @" \n " ]; - ( NSString * ) upgradedPrivateKeyFromFile: ( NSString * ) inPath NSMutableString * sourcePrivateKey = [[[ NSString stringWithContentsOfUTF8File : inPath ] mutableCopy ] autorelease ]; AILog ( @"Upgrading private keys at %@ gave %@" , inPath , sourcePrivateKey ); if ( ! sourcePrivateKey || ! [ sourcePrivateKey length ]) return nil ; * Gaim used the account name for the name and the prpl id for the protocol. * We will use the internalObjectID for the name and the service's uniqueID for the protocol. /* Remove Jabber resources... from the private key list * If you used a non-default resource, no upgrade for you. [ sourcePrivateKey replaceOccurrencesOfString : @"/Adium" range : NSMakeRange ( 0 , [ sourcePrivateKey length ])]; NSDictionary * prplDict = [ self prplDict ]; for ( AIAccount * account in adium . accountController . accounts ) { //Hit every possibile name for this account along the way NSString * accountInternalObjectID = [ NSString stringWithFormat : @" \" %@ \" " , account . internalObjectID ]; for ( NSString * accountName in [ NSSet setWithObjects : account . UID , account . formattedUID ,[ account . UID compactedString ], nil ]) { NSRange accountNameRange = NSMakeRange ( 0 , 0 ); NSRange searchRange = NSMakeRange ( 0 , [ sourcePrivateKey length ]); while ( accountNameRange . location != NSNotFound && ( NSMaxRange ( searchRange ) <= [ sourcePrivateKey length ])) { //Find the next place this account name is located accountNameRange = [ sourcePrivateKey rangeOfString : accountName if ( accountNameRange . location != NSNotFound ) { //Update our search range searchRange . location = NSMaxRange ( accountNameRange ); searchRange . length = [ sourcePrivateKey length ] - searchRange . location ; //Make sure that this account name actually begins and finishes a name; otherwise (name TekJew2) matches (name TekJew) if (( ! [[ sourcePrivateKey substringWithRange : NSMakeRange ( accountNameRange . location - 6 , 6 )] isEqualToString : @"(name " ] && ! [[ sourcePrivateKey substringWithRange : NSMakeRange ( accountNameRange . location - 7 , 7 )] isEqualToString : @"(name \" " ]) || ( ! [[ sourcePrivateKey substringWithRange : NSMakeRange ( NSMaxRange ( accountNameRange ), 1 )] isEqualToString : @")" ] && ! [[ sourcePrivateKey substringWithRange : NSMakeRange ( NSMaxRange ( accountNameRange ), 2 )] isEqualToString : @" \" )" ])) { /* Within that range, find the next "(protocol " which encloses * a string of the form "(protocol protocol-name)" NSRange protocolRange = [ sourcePrivateKey rangeOfString : @"(protocol " if ( protocolRange . location != NSNotFound ) { //Update our search range searchRange . location = NSMaxRange ( protocolRange ); searchRange . length = [ sourcePrivateKey length ] - searchRange . location ; NSRange nextClosingParen = [ sourcePrivateKey rangeOfString : @")" NSRange protocolNameRange = NSMakeRange ( NSMaxRange ( protocolRange ), nextClosingParen . location - NSMaxRange ( protocolRange )); NSString * protocolName = [ sourcePrivateKey substringWithRange : protocolNameRange ]; //Remove a trailing quote if necessary if ([[ protocolName substringFromIndex : ([ protocolName length ] -1 )] isEqualToString : @" \" " ]) { protocolName = [ protocolName substringToIndex : ([ protocolName length ] -1 )]; NSString * uniqueServiceID = [ prplDict objectForKey : protocolName ]; if ([ account . service . serviceCodeUniqueID isEqualToString : uniqueServiceID ]) { //Replace the protocol name first [ sourcePrivateKey replaceCharactersInRange : protocolNameRange withString : uniqueServiceID ]; //Then replace the account name which was before it (so the range hasn't changed) if ([ sourcePrivateKey characterAtIndex : ( accountNameRange . location - 1 )] == '\"' ) { accountNameRange . location -= 1 ; accountNameRange . length += 1 ; if ([ sourcePrivateKey characterAtIndex : ( accountNameRange . location + accountNameRange . length + 1 )] == '\"' ) { accountNameRange . length += 1 ; [ sourcePrivateKey replaceCharactersInRange : accountNameRange withString : accountInternalObjectID ]; AILog ( @"%@ - %@" , accountName , sourcePrivateKey ); - ( void ) upgradeOTRIfNeeded if ( ! [[ adium . preferenceController preferenceForKey : @"GaimOTR_to_AdiumOTR_Update" group : @"OTR" ] boolValue ]) { NSString * destinationPath = [ adium . loginController userDirectory ]; NSString * sourcePath = [ destinationPath stringByAppendingPathComponent : @"libpurple" ]; NSString * privateKey = [ self upgradedPrivateKeyFromFile : [ sourcePath stringByAppendingPathComponent : @"otr.private_key" ]]; if ( privateKey && [ privateKey length ]) { [ privateKey writeToFile : [ destinationPath stringByAppendingPathComponent : @"otr.private_key" ] encoding : NSUTF8StringEncoding NSString * fingerprints = [ self upgradedFingerprintsFromFile : [ sourcePath stringByAppendingPathComponent : @"otr.fingerprints" ]]; if ( fingerprints && [ fingerprints length ]) { [ fingerprints writeToFile : [ destinationPath stringByAppendingPathComponent : @"otr.fingerprints" ] encoding : NSUTF8StringEncoding [ adium . preferenceController setPreference : [ NSNumber numberWithBool : YES ] forKey : @"GaimOTR_to_AdiumOTR_Update" if ( ! [[ adium . preferenceController preferenceForKey : @"Libgaim_to_Libpurple_Update" group : @"OTR" ] boolValue ]) { NSString * destinationPath = [ adium . loginController userDirectory ]; NSString * privateKeyPath = [ destinationPath stringByAppendingPathComponent : @"otr.private_key" ]; NSString * fingerprintsPath = [ destinationPath stringByAppendingPathComponent : @"otr.fingerprints" ]; NSMutableString * privateKeys = [[ NSString stringWithContentsOfUTF8File : privateKeyPath ] mutableCopy ]; [ privateKeys replaceOccurrencesOfString : @"libgaim" range : NSMakeRange ( 0 , [ privateKeys length ])]; [ privateKeys writeToFile : privateKeyPath encoding : NSUTF8StringEncoding NSMutableString * fingerprints = [[ NSString stringWithContentsOfUTF8File : fingerprintsPath ] mutableCopy ]; [ fingerprints replaceOccurrencesOfString : @"libgaim" range : NSMakeRange ( 0 , [ fingerprints length ])]; [ fingerprints writeToFile : fingerprintsPath encoding : NSUTF8StringEncoding [ adium . preferenceController setPreference : [ NSNumber numberWithBool : YES ] forKey : @"Libgaim_to_Libpurple_Update"