Prevent sending 0-byte files. Fixes #8711.
[adiumx.git] / Source / AdiumOTREncryption.m
blobd879d5ff668b595c521e3e72986d8242362088d0
1 //
2 //  AdiumOTREncryption.m
3 //  Adium
4 //
5 //  Created by Evan Schoenberg on 12/28/05.
6 //
8 #import "AdiumOTREncryption.h"
9 #import <Adium/AIContentMessage.h>
10 #import <Adium/AIAccountControllerProtocol.h>
11 #import <Adium/AIChatControllerProtocol.h>
12 #import <Adium/AIContactControllerProtocol.h>
13 #import <Adium/AIContentControllerProtocol.h>
14 #import <Adium/AIInterfaceControllerProtocol.h>
15 #import <Adium/AIPreferenceControllerProtocol.h>
16 #import <Adium/AILoginControllerProtocol.h>
17 #import <Adium/AIAccount.h>
18 #import <Adium/AIChat.h>
19 #import <Adium/AIService.h>
20 #import <Adium/AIContentMessage.h>
21 #import <Adium/AIListObject.h>
22 #import <Adium/AIListContact.h>
23 #import "AIHTMLDecoder.h"
25 #import <AIUtilities/AIStringAdditions.h>
27 #import "ESOTRPrivateKeyGenerationWindowController.h"
28 #import "ESOTRPreferences.h"
29 #import "ESOTRUnknownFingerprintController.h"
30 #import "OTRCommon.h"
32 #include <stdlib.h>
34 #define PRIVKEY_PATH [[[[[AIObject sharedAdiumInstance] loginController] userDirectory] stringByAppendingPathComponent:@"otr.private_key"] UTF8String]
35 #define STORE_PATH       [[[[[AIObject sharedAdiumInstance] loginController] userDirectory] stringByAppendingPathComponent:@"otr.fingerprints"] UTF8String]
37 #define CLOSED_CONNECTION_MESSAGE "has closed his private connection to you"
39 /* OTRL_POLICY_MANUAL doesn't let us respond to other users' automatic attempts at encryption.
40 * If either user has OTR set to Automatic, an OTR session should be begun; without this modified
41 * mask, both users would have to be on automatic for OTR to begin automatically, even though one user
42 * _manually_ attempting OTR will _automatically_ bring the other into OTR even if the setting is Manual.
44 #define OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE     ( OTRL_POLICY_MANUAL | \
45                                                                                                           OTRL_POLICY_WHITESPACE_START_AKE | \
46                                                                                                           OTRL_POLICY_ERROR_START_AKE )
48 @interface AdiumOTREncryption (PRIVATE)
49 - (void)prepareEncryption;
51 - (void)setSecurityDetails:(NSDictionary *)securityDetailsDict forChat:(AIChat *)inChat;
52 - (NSString *)localizedOTRMessage:(NSString *)message withUsername:(NSString *)username isWorthOpeningANewChat:(BOOL *)isWorthOpeningANewChat;
53 - (void)notifyWithTitle:(NSString *)title primary:(NSString *)primary secondary:(NSString *)secondary;
55 - (void)upgradeOTRIfNeeded;
56 @end
58 @implementation AdiumOTREncryption
60 /* We'll only use the one OtrlUserState. */
61 static OtrlUserState otrg_plugin_userstate = NULL;
62 static AdiumOTREncryption       *adiumOTREncryption = nil;
64 void otrg_ui_update_fingerprint(void);
65 void update_security_details_for_chat(AIChat *chat);
66 void send_default_query_to_chat(AIChat *inChat);
67 void disconnect_from_chat(AIChat *inChat);
68 void disconnect_from_context(ConnContext *context);
69 TrustLevel otrg_plugin_context_to_trust(ConnContext *context);
71 - (id)init
73         //Singleton
74         if (adiumOTREncryption) {
75                 [self release];
76                 
77                 return [adiumOTREncryption retain];
78         }
80         if ((self = [super init])) {
81                 adiumOTREncryption = self;
83                 //Wait for Adium to finish launching to prepare encryption so that accounts will be loaded
84                 [[adium notificationCenter] addObserver:self
85                                                                            selector:@selector(adiumFinishedLaunching:)
86                                                                                    name:AIApplicationDidFinishLoadingNotification
87                                                                                  object:nil];
88                 /*
89                 gaim_signal_connect(conn_handle, "signed-on", otrg_plugin_handle,
90                                                         GAIM_CALLBACK(process_connection_change), NULL);
91                 gaim_signal_connect(conn_handle, "signed-off", otrg_plugin_handle,
92                                                         GAIM_CALLBACK(process_connection_change), NULL);                
93                  */
94         }
95         
96         return self;
99 - (void)adiumFinishedLaunching:(NSNotification *)inNotification
101         [self prepareEncryption];
104 - (void)prepareEncryption
106         /* Initialize the OTR library */
107         OTRL_INIT;
109         [self upgradeOTRIfNeeded];
111         /* Make our OtrlUserState; we'll only use the one. */
112         otrg_plugin_userstate = otrl_userstate_create();
114         int err;
115         
116         err = otrl_privkey_read(otrg_plugin_userstate, PRIVKEY_PATH);
117         if (err) {
118                 const char *errMsg = gpg_strerror(err);
119                 
120                 if (errMsg && strcmp(errMsg, "No such file or directory")) {
121                         NSLog(@"Error reading %s: %s", PRIVKEY_PATH, errMsg);
122                 }
123         }
125         otrg_ui_update_keylist();
127         err = otrl_privkey_read_fingerprints(otrg_plugin_userstate, STORE_PATH,
128                                                                    NULL, NULL);
129         if (err) {
130                 const char *errMsg = gpg_strerror(err);
131                 
132                 if (errMsg && strcmp(errMsg, "No such file or directory")) {
133                         NSLog(@"Error reading %s: %s", STORE_PATH, errMsg);
134                 }
135         }
137         otrg_ui_update_fingerprint();
138         
139         
140         [[adium notificationCenter] addObserver:self
141                                                                    selector:@selector(adiumWillTerminate:)
142                                                                            name:AIAppWillTerminateNotification
143                                                                          object:nil];
144         
145         [[adium notificationCenter] addObserver:self
146                                                                    selector:@selector(updateSecurityDetails:) 
147                                                                            name:Chat_SourceChanged
148                                                                          object:nil];
149         [[adium notificationCenter] addObserver:self
150                                                                    selector:@selector(updateSecurityDetails:) 
151                                                                            name:Chat_DestinationChanged
152                                                                          object:nil];
153         [[adium notificationCenter] addObserver:self
154                                                                    selector:@selector(updateSecurityDetails:) 
155                                                                            name:Chat_DidOpen
156                                                                          object:nil];
158         //Add the Encryption preferences
159         OTRPrefs = [[ESOTRPreferences preferencePane] retain];
162 - (void)dealloc
164         [OTRPrefs release];
165         [[adium notificationCenter] removeObserver:self];
167         [super dealloc];
171 #pragma mark -
173 /* 
174 * @brief Return an NSDictionary* describing a ConnContext.
176  *      Key                              :        Contents
177  * @"Their Fingerprint"  : NSString of the contact's fingerprint's human-readable hash
178  * @"Our Fingerprint"    : NSString of our fingerprint's human-readable hash
179  * @"Incoming SessionID" : NSString of the incoming sessionID
180  * @"Outgoing SessionID" : NSString of the outgoing sessionID
181  * @"EncryptionStatus"   : An AIEncryptionStatus
182  * @"AIAccount"                  : The AIAccount of this context
183  * @"who"                                : The UID of the remote user *
184  * @result The dictinoary
185  */
186 static NSDictionary* details_for_context(ConnContext *context)
188         if (!context) return nil;
190         NSDictionary            *securityDetailsDict;
191         Fingerprint *fprint = context->active_fingerprint;      
193     if (!fprint || !(fprint->fingerprint)) return nil;
194     context = fprint->context;
195     if (!context) return nil;
197     TrustLevel                  level = otrg_plugin_context_to_trust(context);
198         AIEncryptionStatus      encryptionStatus;
199         AIAccount                       *account;
200         
201         switch (level) {
202                 default:
203             case TRUST_NOT_PRIVATE:
204                         encryptionStatus = EncryptionStatus_None;
205                         break;
206                 case TRUST_UNVERIFIED:
207                         encryptionStatus = EncryptionStatus_Unverified;
208                         break;
209                 case TRUST_PRIVATE:
210                         encryptionStatus = EncryptionStatus_Verified;
211                         break;
212                 case TRUST_FINISHED:
213                         encryptionStatus = EncryptionStatus_Finished;
214                         break;
215         }
216         
217     char our_hash[45], their_hash[45];
219         otrl_privkey_fingerprint(otrg_get_userstate(), our_hash,
220                                                          context->accountname, context->protocol);
221         
222     otrl_privkey_hash_to_human(their_hash, fprint->fingerprint);
224         unsigned char *sessionid;
225     char sess1[21], sess2[21];
226         BOOL sess1_outgoing = (context->sessionid_half == OTRL_SESSIONID_FIRST_HALF_BOLD);
227     size_t idhalflen = (context->sessionid_len) / 2;
229     /* Make a human-readable version of the sessionid (in two parts) */
230     sessionid = context->sessionid;
231     for(int i = 0; i < idhalflen; ++i) sprintf(sess1+(2*i), "%02x", sessionid[i]);
232     for(int i = 0; i < idhalflen; ++i) sprintf(sess2+(2*i), "%02x", sessionid[i+idhalflen]);
234         account = [[[AIObject sharedAdiumInstance] accountController] accountWithInternalObjectID:[NSString stringWithUTF8String:context->accountname]];
236         securityDetailsDict = [NSDictionary dictionaryWithObjectsAndKeys:
237                 [NSString stringWithUTF8String:their_hash], @"Their Fingerprint",
238                 [NSString stringWithUTF8String:our_hash], @"Our Fingerprint",
239                 [NSNumber numberWithInt:encryptionStatus], @"EncryptionStatus",
240                 account, @"AIAccount",
241                 [NSString stringWithUTF8String:context->username], @"who",
242                 [NSString stringWithUTF8String:sess1], (sess1_outgoing ? @"Outgoing SessionID" : @"Incoming SessionID"),
243                 [NSString stringWithUTF8String:sess2], (sess1_outgoing ? @"Incoming SessionID" : @"Outgoing SessionID"),
244                 nil];
245         
246         AILog(@"Security details: %@",securityDetailsDict);
247         
248         return securityDetailsDict;
252 static AIAccount* accountFromAccountID(const char *accountID)
254         return [[[AIObject sharedAdiumInstance] accountController] accountWithInternalObjectID:[NSString stringWithUTF8String:accountID]];
257 static AIService* serviceFromServiceID(const char *serviceID)
259         return [[[AIObject sharedAdiumInstance] accountController] serviceWithUniqueID:[NSString stringWithUTF8String:serviceID]];
262 static AIListContact* contactFromInfo(const char *accountID, const char *serviceID, const char *username)
264         return [[[AIObject sharedAdiumInstance] contactController] contactWithService:serviceFromServiceID(serviceID)
265                                                                                                                                                   account:accountFromAccountID(accountID)
266                                                                                                                                                           UID:[NSString stringWithUTF8String:username]];
268 static AIListContact* contactForContext(ConnContext *context)
270         return contactFromInfo(context->accountname, context->protocol, context->username);
273 static AIChat* chatForContext(ConnContext *context)
275         AIListContact *listContact = contactForContext(context);
276         AIChat *chat = [[[AIObject sharedAdiumInstance] chatController] existingChatWithContact:listContact];
277         if (!chat) {
278                 chat = [[[AIObject sharedAdiumInstance] chatController] chatWithContact:listContact];
279         }
280         
281         return chat;
285 static OtrlPolicy policyForContact(AIListContact *contact)
287         OtrlPolicy              policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
288         
289         //Force OTRL_POLICY_MANUAL when interacting with mobile numbers
290         if ([[contact UID] characterAtIndex:0] == '+') {
291                 policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
292                 
293         } else {
294                 NSNumber                                        *prefNumber;
295                 AIEncryptedChatPreference       pref;
296                 
297                 //Get the contact's preference (or its containing group, or so on)
298                 prefNumber = [contact preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE
299                                                                                  group:GROUP_ENCRYPTION];
300                 if (!prefNumber || ([prefNumber intValue] == EncryptedChat_Default)) {
301                         //If no contact preference or the contact is set to use the default, use the account preference
302                         prefNumber = [[contact account] preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE
303                                                                                                            group:GROUP_ENCRYPTION];             
304                 }
305                 
306                 if (prefNumber) {
307                         pref = [prefNumber intValue];
308                         
309                         switch (pref) {
310                                 case EncryptedChat_Never:
311                                         policy = OTRL_POLICY_NEVER;
312                                         break;
313                                 case EncryptedChat_Manually:
314                                 case EncryptedChat_Default:
315                                         policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
316                                         break;
317                                 case EncryptedChat_Automatically:
318                                         policy = OTRL_POLICY_OPPORTUNISTIC;
319                                         break;
320                                 case EncryptedChat_RejectUnencryptedMessages:
321                                         policy = OTRL_POLICY_ALWAYS;
322                                         break;
323                         }
324                 } else {
325                         policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
326                 }
327         }
328         
329         return policy;
330         
333 //Return the ConnContext for a Conversation, or NULL if none exists
334 static ConnContext* contextForChat(AIChat *chat)
336         AIAccount       *account;
337     const char *username, *accountname, *proto;
338     ConnContext *context;
339         
340     /* Do nothing if this isn't an IM conversation */
341     if ([chat isGroupChat]) return nil;
342         
343     account = [chat account];
344         accountname = [[account internalObjectID] UTF8String];
345         proto = [[[account service] serviceCodeUniqueID] UTF8String];
346     username = [[[chat listObject] UID] UTF8String];
347         
348     context = otrl_context_find(otrg_plugin_userstate,
349                                                                 username, accountname, proto, 0, NULL,
350                                                                 NULL, NULL);
351         
352         return context;
355 /* What level of trust do we have in the privacy of this ConnContext? */
356 TrustLevel otrg_plugin_context_to_trust(ConnContext *context)
358     TrustLevel level = TRUST_NOT_PRIVATE;
359         
360     if (context && context->msgstate == OTRL_MSGSTATE_ENCRYPTED) {
361                 if (context->active_fingerprint->trust &&
362                         context->active_fingerprint->trust[0] != '\0') {
363                         level = TRUST_PRIVATE;
364                 } else {
365                         level = TRUST_UNVERIFIED;
366                 }
367     } else if (context && context->msgstate == OTRL_MSGSTATE_FINISHED) {
368                 level = TRUST_FINISHED;
369     }
370         
371     return level;
374 #pragma mark -
376 static OtrlPolicy policy_cb(void *opdata, ConnContext *context)
378         return policyForContact(contactForContext(context));    
381 /* Generate a private key for the given accountname/protocol */
382 void otrg_plugin_create_privkey(const char *accountname,
383                                                                 const char *protocol)
384 {       
385         AIAccount       *account = accountFromAccountID(accountname);
386         AIService       *service = serviceFromServiceID(protocol);
387         
388         NSString        *identifier = [NSString stringWithFormat:@"%@ (%@)",[account formattedUID], [service shortDescription]];
389         
390         [ESOTRPrivateKeyGenerationWindowController startedGeneratingForIdentifier:identifier];
391         
392     /* Generate the key */
393     otrl_privkey_generate(otrg_plugin_userstate, PRIVKEY_PATH,
394                                                   accountname, protocol);
395     otrg_ui_update_keylist();
396         
397     /* Mark the dialog as done. */
398         [ESOTRPrivateKeyGenerationWindowController finishedGeneratingForIdentifier:identifier];
401 static void create_privkey_cb(void *opdata, const char *accountname,
402                                                           const char *protocol)
404         otrg_plugin_create_privkey(accountname, protocol);
407 static int is_logged_in_cb(void *opdata, const char *accountname,
408                                                    const char *protocol, const char *recipient)
410         return ([contactFromInfo(accountname, protocol, recipient) online]);
413 static void inject_message_cb(void *opdata, const char *accountname,
414                                                           const char *protocol, const char *recipient, const char *message)
415 {       
416         [[[AIObject sharedAdiumInstance] contentController] sendRawMessage:[NSString stringWithUTF8String:message]
417                                                                                                                          toContact:contactFromInfo(accountname, protocol, recipient)];
421  * @brief Display an OTR message
423  * This should be displayed within the relevant chat.
425  * @result 0 if we handled displaying the message; 1 if we could not
426  */
427 static int display_otr_message(const char *accountname, const char *protocol,
428                                                            const char *username, const char *msg)
430         NSString                        *message;
431         NSObject<AIAdium>        *sharedAdium = [AIObject sharedAdiumInstance];
432         AIListContact           *listContact = contactFromInfo(accountname, protocol, username);
433         AIChat                          *chat;
434         AIContentMessage        *messageObject;
435         
436         //We couldn't determine a listContact, so return that we didn't handle the message
437         if (!listContact) return 1;
438         
439         chat = [[sharedAdium chatController] existingChatWithContact:listContact];
440         
441         message = [NSString stringWithUTF8String:msg];
442         AILog(@"display_otr_message: %s %s %s: %s",accountname,protocol,username, msg);
443          
444         if (([message rangeOfString:@"<b>The following message received from"].location != NSNotFound) &&
445                 ([message rangeOfString:@"was <i>not</i> encrypted: ["].location != NSNotFound)) {
446                 /*
447                  * If we receive an unencrypted message, display it as a normal incoming message with the bolded warning that
448                  * the message was not encrypted
449                  */             
450                 NSRange                 endRange = [message rangeOfString:@"was <i>not</i> encrypted: ["];
451                 
452                 /* The message will be formatted as:
453                  * <b>The following message received from tekjew was <i>not</i> encrypted: [</b>MESSAGE_HERE - POTENTIALLY HTML<b>]</b>
454                  */
455                 NSString *OTRMessage = [adiumOTREncryption localizedOTRMessage:@"The following message was <b>not encrypted</b>: "
456                                                                                                                   withUsername:nil
457                                                                                                 isWorthOpeningANewChat:NULL];
458                 message = [OTRMessage stringByAppendingString:
459                         [message substringWithRange:NSMakeRange(NSMaxRange(endRange),
460                                                                                                         ([message length] - NSMaxRange(endRange) - [@"<b>]</b>" length]))]];
461         
462                 //Create a new chat if necessary
463                 if (!chat) chat = [[sharedAdium chatController] chatWithContact:listContact];
465                 messageObject = [AIContentMessage messageInChat:chat
466                                                                                          withSource:listContact
467                                                                                         destination:[chat account]
468                                                                                                    date:nil
469                                                                                                 message:[AIHTMLDecoder decodeHTML:message]
470                                                                                           autoreply:NO];
471                 
472                 [[sharedAdium contentController] receiveContentObject:messageObject];
473                 
474         } else {
475                 NSString        *formattedUID = [listContact formattedUID];
476                 BOOL            isWorthOpeningANewChat = NO;
478                 //All other OTR messages should be displayed as status messages; decode the message to strip any HTML
479                 message = [adiumOTREncryption localizedOTRMessage:message
480                                                                                          withUsername:formattedUID
481                                                                    isWorthOpeningANewChat:&isWorthOpeningANewChat];
483                 if (isWorthOpeningANewChat) {
484                         //Create a new chat if we don't already have one and this message is worth it
485                         if (!chat)
486                                 chat = [[sharedAdium chatController] chatWithContact:listContact];
487                 } else {
488                         /* 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
489                          * being used by some delayed process, don't display a message thereby opening it.
490                          */
491                         if (![chat isOpen]) chat = nil;
492                 }
494                 if (chat) {
495                         [[sharedAdium contentController] displayEvent:[[AIHTMLDecoder decodeHTML:message] string]
496                                                                                                    ofType:@"encryption"
497                                                                                                    inChat:chat];
498                 }
499         }
500         
501         //We handled it
502         return 0;
505 static void notify_cb(void *opdata, OtrlNotifyLevel level,
506                                           const char *accountname, const char *protocol, const char *username,
507                                           const char *title, const char *primary, const char *secondary)
509         AIListContact   *listContact = contactFromInfo(accountname, protocol, username);
510         NSString                *formattedUID = [listContact formattedUID];
512         [adiumOTREncryption notifyWithTitle:[adiumOTREncryption localizedOTRMessage:[NSString stringWithUTF8String:title]
513                                                                                                                                    withUsername:formattedUID
514                                                                                                                  isWorthOpeningANewChat:NULL]
515                                                                 primary:[adiumOTREncryption localizedOTRMessage:[NSString stringWithUTF8String:primary]
516                                                                                                                                    withUsername:formattedUID
517                                                                                                                  isWorthOpeningANewChat:NULL]
518                                                           secondary:[adiumOTREncryption localizedOTRMessage:[NSString stringWithUTF8String:secondary]
519                                                                                                                                    withUsername:formattedUID
520                                                                                                                                  isWorthOpeningANewChat:NULL]];
523 static int display_otr_message_cb(void *opdata, const char *accountname,
524                                                                   const char *protocol, const char *username, const char *msg)
526         return display_otr_message(accountname, protocol, username, msg);
529 static void update_context_list_cb(void *opdata)
531         otrg_ui_update_keylist();
534 static const char *account_display_name_cb(void *opdata, const char *accountname, const char *protocol)
536         return [[accountFromAccountID(accountname) formattedUID] UTF8String];
539 static void account_display_name_free_cb(void *opdata, const char *account_display_name)
541     /* Do nothing, since we didn't actually allocate any memory in
542          * account_display_name_cb(). */
545 static const char *protocol_name_cb(void *opdata, const char *protocol)
547         return [[serviceFromServiceID(protocol) shortDescription] UTF8String];
550 static void protocol_name_free_cb(void *opdata, const char *protocol_name)
552     /* Do nothing, since we didn't actually allocate any memory in
553          * protocol_name_cb(). */
557 static void confirm_fingerprint_cb(void *opdata, OtrlUserState us,
558                                                                    const char *accountname, const char *protocol, const char *username,
559                                                                    unsigned char fingerprint[20])
561         ConnContext                     *context;
562         
563         context = otrl_context_find(us, username, accountname,
564                                                                 protocol, 0, NULL, NULL, NULL);
565         
566         if (context == NULL/* || context->msgstate != OTRL_MSGSTATE_ENCRYPTED*/) {
567                 NSLog(@"otrg_adium_dialog_unknown_fingerprint: Ack!");
568                 return;
569         }
570         
571         [adiumOTREncryption performSelector:@selector(verifyUnknownFingerprint:)
572                                                          withObject:[NSValue valueWithPointer:context]
573                                                          afterDelay:0];
576 static void write_fingerprints_cb(void *opdata)
578         otrg_plugin_write_fingerprints();
581 static void gone_secure_cb(void *opdata, ConnContext *context)
583         AIChat *chat = chatForContext(context);
585     update_security_details_for_chat(chat);
586         otrg_ui_update_fingerprint();
589 static void gone_insecure_cb(void *opdata, ConnContext *context)
591         AIChat *chat = chatForContext(context);
593     update_security_details_for_chat(chat);
594         otrg_ui_update_fingerprint();
597 static void still_secure_cb(void *opdata, ConnContext *context, int is_reply)
599     if (is_reply == 0) {
600                 //              otrg_dialog_stillconnected(context);
601                 AILog(@"Still secure...");
602     }
605 static void log_message_cb(void *opdata, const char *message)
607     AILog([NSString stringWithFormat:@"otr: %s", (message ? message : "(null)")]);
610 static OtrlMessageAppOps ui_ops = {
611     policy_cb,
612     create_privkey_cb,
613     is_logged_in_cb,
614     inject_message_cb,
615     notify_cb,
616     display_otr_message_cb,
617     update_context_list_cb,
618         account_display_name_cb,
619         account_display_name_free_cb,
620     protocol_name_cb,
621     protocol_name_free_cb,
622     confirm_fingerprint_cb,
623     write_fingerprints_cb,
624     gone_secure_cb,
625     gone_insecure_cb,
626     still_secure_cb,
627     log_message_cb
630 #pragma mark -
632 - (void)willSendContentMessage:(AIContentMessage *)inContentMessage
634         const char      *originalMessage = [[inContentMessage encodedMessage] UTF8String];
635         AIAccount       *account = (AIAccount *)[inContentMessage source];
636     const char  *accountname = [[account internalObjectID] UTF8String];
637     const char  *protocol = [[[account service] serviceCodeUniqueID] UTF8String];
638     const char  *username = [[[inContentMessage destination] UID] UTF8String];
639         char            *newMessage = NULL;
641     gcry_error_t err;
642         
643     if (!username || !originalMessage)
644                 return;
646     err = otrl_message_sending(otrg_plugin_userstate, &ui_ops, /* opData */ NULL,
647                                                            accountname, protocol, username, originalMessage, /* tlvs */ NULL, &newMessage,
648                                                            /* add_appdata cb */NULL, /* appdata */ NULL);
650     if (err && newMessage == NULL) {
651                 //Be *sure* not to send out plaintext
652                 [inContentMessage setEncodedMessage:nil];
654     } else if (newMessage) {
655                 //This new message is what should be sent to the remote contact
656                 [inContentMessage setEncodedMessage:[NSString stringWithUTF8String:newMessage]];
658                 //We're now done with newMessage
659                 otrl_message_free(newMessage);
660     }
663 - (NSString *)decryptIncomingMessage:(NSString *)inString fromContact:(AIListContact *)inListContact onAccount:(AIAccount *)inAccount
665         NSString        *decryptedMessage = nil;
666         const char *message = [inString UTF8String];
667         char *newMessage = NULL;
668     OtrlTLV *tlvs = NULL;
669     OtrlTLV *tlv = NULL;
670         const char *username = [[inListContact UID] UTF8String];
671     const char *accountname = [[inAccount internalObjectID] UTF8String];
672     const char *protocol = [[[inAccount service] serviceCodeUniqueID] UTF8String];
673         BOOL    res;
675         /* If newMessage is set to non-NULL and res is 0, use newMessage.
676          * If newMessage is set to non-NULL and res is not 0, display nothing as this was an OTR message
677          * If newMessage is set to NULL and res is 0, use message
678          */
679     res = otrl_message_receiving(otrg_plugin_userstate, &ui_ops, NULL,
680                                                                  accountname, protocol, username, message,
681                                                                  &newMessage, &tlvs, NULL, NULL);
682         
683         if (!newMessage && !res) {
684                 //Use the original mesage; this was not an OTR-related message
685                 decryptedMessage = inString;
686         } else if (newMessage && !res) {
687                 //We decryped an OTR-encrypted message
688                 decryptedMessage = [NSString stringWithUTF8String:newMessage];
690         } else /* (newMessage && res) */{
691                 //This was an OTR protocol message
692                 decryptedMessage = nil;
693         }
695         if (newMessage)
696                 otrl_message_free(newMessage);
698     tlv = otrl_tlv_find(tlvs, OTRL_TLV_DISCONNECTED);
699     if (tlv) {
700                 /* Notify the user that the other side disconnected. */
701                 display_otr_message(accountname, protocol, username, CLOSED_CONNECTION_MESSAGE);
703                 otrg_ui_update_keylist();
704     }
706     otrl_tlv_free(tlvs);
707         
708         return decryptedMessage;
711 - (void)requestSecureOTRMessaging:(BOOL)inSecureMessaging inChat:(AIChat *)inChat
713         if (inSecureMessaging) {
714                 send_default_query_to_chat(inChat);
716         } else {
717                 disconnect_from_chat(inChat);
718         }
721 - (void)promptToVerifyEncryptionIdentityInChat:(AIChat *)inChat
723         ConnContext             *context = contextForChat(inChat);
724         NSDictionary    *responseInfo = details_for_context(context);;
726         [ESOTRUnknownFingerprintController showVerifyFingerprintPromptWithResponseInfo:responseInfo];   
730  * @brief Adium will begin terminating
732  * Send the OTRL_TLV_DISCONNECTED packets when we're about to quit before we disconnect
733  */
734 - (void)adiumWillTerminate:(NSNotification *)inNotification
736         ConnContext *context = otrg_plugin_userstate->context_root;
737         while(context) {
738                 ConnContext *next = context->next;
739                 if (context->msgstate == OTRL_MSGSTATE_ENCRYPTED &&
740                         context->protocol_version > 1) {
741                         disconnect_from_context(context);
742                 }
743                 context = next;
744         }
748  * @brief A chat notification was posted after which we should update our security details
750  * @param inNotification A notification whose object is the AIChat in question
751  */
752 - (void)updateSecurityDetails:(NSNotification *)inNotification
754         AILog(@"Updating security details for %@",[inNotification object]);
755         update_security_details_for_chat([inNotification object]);
758 void update_security_details_for_chat(AIChat *inChat)
760         ConnContext *context = contextForChat(inChat);
762         [adiumOTREncryption setSecurityDetails:details_for_context(context)
763                                                                    forChat:inChat];
766 - (void)setSecurityDetails:(NSDictionary *)securityDetailsDict forChat:(AIChat *)inChat
768         if (inChat) {
769                 NSMutableDictionary     *fullSecurityDetailsDict;
770                 
771                 if (securityDetailsDict) {
772                         NSString                                *format, *description;
773                         fullSecurityDetailsDict = [[securityDetailsDict mutableCopy] autorelease];
774                         
775                         /* Encrypted by Off-the-Record Messaging
776                                 *
777                                 * Fingerprint for TekJew:
778                                 * <Fingerprint>
779                                 *
780                                 * Secure ID for this session:
781                                 * Incoming: <Incoming SessionID>
782                                 * Outgoing: <Outgoing SessionID>
783                                 */
784                         format = [@"%@\n\n" stringByAppendingString:AILocalizedString(@"Fingerprint for %@:","Fingerprint for <name>:")];
785                         format = [format stringByAppendingString:@"\n%@\n\n%@\n%@ %@\n%@ %@"];
786                         
787                         description = [NSString stringWithFormat:format,
788                                 AILocalizedString(@"Encrypted by Off-the-Record Messaging",nil),
789                                 [[inChat listObject] formattedUID],
790                                 [securityDetailsDict objectForKey:@"Their Fingerprint"],
791                                 AILocalizedString(@"Secure ID for this session:",nil),
792                                 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."),
793                                 [securityDetailsDict objectForKey:@"Incoming SessionID"],
794                                 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."),
795                                 [securityDetailsDict objectForKey:@"Outgoing SessionID"],
796                                 nil];
797                         
798                         [fullSecurityDetailsDict setObject:description
799                                                                                 forKey:@"Description"];
800                 } else {
801                         fullSecurityDetailsDict = nil;  
802                 }
803                 
804                 [inChat setSecurityDetails:fullSecurityDetailsDict];
805         }
806 }       
808 #pragma mark -
810 void send_default_query_to_chat(AIChat *inChat)
812         //Note that we pass a name for display, not internal usage
813         char *msg = otrl_proto_default_query_msg([[[inChat account] formattedUID] UTF8String],
814                                                                                          policyForContact([inChat listObject]));
815         
816         [[[AIObject sharedAdiumInstance] contentController] sendRawMessage:[NSString stringWithUTF8String:(msg ? msg : "?OTRv2?")]
817                                                                                                                          toContact:[inChat listObject]];
818         if (msg)
819                 free(msg);
822 /* Disconnect a context, sending a notice to the other side, if
823 * appropriate. */
824 void disconnect_from_context(ConnContext *context)
826     otrl_message_disconnect(otrg_plugin_userstate, &ui_ops, NULL,
827                                                         context->accountname, context->protocol, context->username);
828         gone_insecure_cb(NULL, context);
831 void disconnect_from_chat(AIChat *inChat)
833         disconnect_from_context(contextForChat(inChat));
836 #pragma mark -
838 /* Forget a fingerprint */
839 void otrg_ui_forget_fingerprint(Fingerprint *fingerprint)
841     ConnContext *context;
843     /* Don't do anything with the active fingerprint if we're in the
844          * ENCRYPTED state. */
845     context = (fingerprint ? fingerprint->context : NULL);
846     if (context && (context->msgstate == OTRL_MSGSTATE_ENCRYPTED &&
847                                         context->active_fingerprint == fingerprint)) return;
848         
849     otrl_context_forget_fingerprint(fingerprint, 1);
850     otrg_plugin_write_fingerprints();
853 void otrg_plugin_write_fingerprints(void)
855     otrl_privkey_write_fingerprints(otrg_plugin_userstate, STORE_PATH);
856         otrg_ui_update_fingerprint();
859 void otrg_ui_update_keylist(void)
861         [adiumOTREncryption prefsShouldUpdatePrivateKeyList];
864 void otrg_ui_update_fingerprint(void)
866         [adiumOTREncryption prefsShouldUpdateFingerprintsList];
869 OtrlUserState otrg_get_userstate(void)
871         return otrg_plugin_userstate;
874 #pragma mark -
876 - (void)verifyUnknownFingerprint:(NSValue *)contextValue
878         NSDictionary            *responseInfo;
879         
880         responseInfo = details_for_context([contextValue pointerValue]);
881         
882         [ESOTRUnknownFingerprintController showUnknownFingerprintPromptWithResponseInfo:responseInfo];
886  * @brief Call this function when our DSA key is updated; it will redraw the Encryption preferences item, if visible.
887  */
888 - (void)prefsShouldUpdatePrivateKeyList
890         [OTRPrefs updatePrivateKeyList];
894  * @brief Update the list of other users' fingerprints, if it's visible
895  */
896 - (void)prefsShouldUpdateFingerprintsList
898         [OTRPrefs updateFingerprintsList];
901 #pragma mark Localization
904  * @brief Given an English message from libotr, construct a localized version
906  * @param message The original message, which was sent by libotr in English
907  * @param username A username (screenname) for substitution purposes as appropriate. May be nil.
908  * @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.
909  */
910 - (NSString *)localizedOTRMessage:(NSString *)message withUsername:(NSString *)username isWorthOpeningANewChat:(BOOL *)isWorthOpeningANewChat
912         NSString        *localizedOTRMessage = nil;
913         if (isWorthOpeningANewChat) *isWorthOpeningANewChat = NO;
915         if (([message rangeOfString:@"You sent unencrypted data to"].location != NSNotFound) &&
916                 ([message rangeOfString:@"who wasn't expecting it"].location != NSNotFound)) {
917                 localizedOTRMessage = [NSString stringWithFormat:
918                         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."),
919                         username];
920                 
921         } else if (([message rangeOfString:@"You sent encrypted data to"].location != NSNotFound) &&
922                            ([message rangeOfString:@"who wasn't expecting it"].location != NSNotFound)) {
923                 localizedOTRMessage = [NSString stringWithFormat:
924                         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."),
925                         username];
926                 if (isWorthOpeningANewChat) *isWorthOpeningANewChat = YES;
928         } else if ([message rangeOfString:@CLOSED_CONNECTION_MESSAGE].location != NSNotFound) {
929                 localizedOTRMessage = [NSString stringWithFormat:
930                         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."),
931                         username];
932                 
933         } else if ([message isEqualToString:@"Private connection closed"]) {
934                 localizedOTRMessage = AILocalizedString(@"Private connection closed", nil);
936         } else if ([message rangeOfString:@"has already closed his private connection to you"].location != NSNotFound) {
937                 localizedOTRMessage = [NSString stringWithFormat:
938                         AILocalizedString(@"%@'s private connection to you is closed.", "Statement that someone's private (encrypted) connection is closed."),
939                         username];
941         } else if ([message isEqualToString:@"Your message was not sent.  Either close your private connection to him, or refresh it."]) {
942                 localizedOTRMessage = AILocalizedString(@"Your message was not sent. You should end the encrypted chat on your side or re-request encryption.", nil);
943                 if (isWorthOpeningANewChat) *isWorthOpeningANewChat = YES;
945         } else if ([message isEqualToString:@"The following message was <b>not encrypted</b>: "]) {
946                 localizedOTRMessage = AILocalizedString(@"The following message was <b>not encrypted</b>: ", nil);
947                 if (isWorthOpeningANewChat) *isWorthOpeningANewChat = YES;
949         } else if ([message rangeOfString:@"received an unreadable encrypted"].location != NSNotFound) {
950                 localizedOTRMessage = [NSString stringWithFormat:
951                         AILocalizedString(@"An encrypted message from %@ could not be decrypted.", nil),
952                         username];
953                 if (isWorthOpeningANewChat) *isWorthOpeningANewChat = YES;
954         }
956         return (localizedOTRMessage ? localizedOTRMessage : message);
960  * @brief Display a message (independent of a chat)
962  * @param title The window title
963  * @param primary The main information for the message
964  * @param secondary Additional information for the message
965  */
966 - (void)notifyWithTitle:(NSString *)title primary:(NSString *)primary secondary:(NSString *)secondary
968         //XXX todo: search on ops->notify in message.c in libotr and handle / localize the error messages
969         [[adium interfaceController] handleMessage:primary
970                                                            withDescription:secondary
971                                                            withWindowTitle:title];
974 #pragma mark Upgrading gaim-otr --> Adium-otr
976  * @brief Construct a dictionary converting libpurple prpl names to Adium serviceIDs for the purpose of fingerprint upgrading
977  */
978 - (NSDictionary *)prplDict
980         return [NSDictionary dictionaryWithObjectsAndKeys:
981                 @"libpurple-OSCAR-AIM", @"prpl-oscar",
982                 @"libpurple-Gadu-Gadu", @"prpl-gg",
983                 @"libpurple-Jabber", @"prpl-jabber",
984                 @"libpurple-Sametime", @"prpl-meanwhile",
985                 @"libpurple-MSN", @"prpl-msn",
986                 @"libpurple-GroupWise", @"prpl-novell",
987                 @"libpurple-Yahoo!", @"prpl-yahoo",
988                 @"libpurple-zephyr", @"prpl-zephyr", nil];
991 - (NSString *)upgradedFingerprintsFromFile:(NSString *)inPath
993         NSString                *sourceFingerprints = [NSString stringWithContentsOfUTF8File:inPath];
994         
995         if (!sourceFingerprints  || ![sourceFingerprints length]) return nil;
997         NSScanner               *scanner = [NSScanner scannerWithString:sourceFingerprints];
998         NSMutableString *outFingerprints = [NSMutableString string];
999         NSCharacterSet  *tabAndNewlineSet = [NSCharacterSet characterSetWithCharactersInString:@"\t\n\r"];
1000         
1001         //Skip quotes
1002         [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"\""]];
1003         
1004         NSDictionary    *prplDict = [self prplDict];
1005         
1006         NSArray                 *adiumAccounts = [[adium accountController] accounts];
1007         
1008         while (![scanner isAtEnd]) {
1009                 //username     accountname  protocol      key   trusted\n
1010                 NSString                *chunk;
1011                 NSString                *username = nil, *accountname = nil, *protocol = nil, *key = nil, *trusted = nil;
1012                 
1013                 //username
1014                 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&username];
1015                 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1016                 
1017                 //accountname
1018                 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&accountname];
1019                 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1020                 
1021                 //protocol
1022                 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&protocol];
1023                 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1024                 
1025                 //key
1026                 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&key];
1027                 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:&chunk];
1028                 
1029                 //We have a trusted entry
1030                 if ([chunk isEqualToString:@"\t"]) {
1031                         //key
1032                         [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&trusted];
1033                         [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];               
1034                 } else {
1035                         trusted = nil;
1036                 }
1037                 
1038                 if (username && accountname && protocol && key) {
1039                         AIAccount               *account;
1040                         NSEnumerator    *enumerator = [adiumAccounts objectEnumerator];
1041                         
1042                         while ((account = [enumerator nextObject])) {
1043                                 //Hit every possibile name for this account along the way
1044                                 if ([[NSSet setWithObjects:[account UID],[account formattedUID],[[account UID] compactedString], nil] containsObject:accountname]) {
1045                                         if ([[[account service] serviceCodeUniqueID] isEqualToString:[prplDict objectForKey:protocol]]) {
1046                                                 [outFingerprints appendString:
1047                                                         [NSString stringWithFormat:@"%@\t%@\t%@\t%@", username, [account internalObjectID], [[account service] serviceCodeUniqueID], key]];
1048                                                 if (trusted) {
1049                                                         [outFingerprints appendString:@"\t"];
1050                                                         [outFingerprints appendString:trusted];
1051                                                 }
1052                                                 [outFingerprints appendString:@"\n"];
1053                                         }
1054                                 }
1055                         }
1056                 }
1057         }
1058         
1059         return outFingerprints;
1062 - (NSString *)upgradedPrivateKeyFromFile:(NSString *)inPath
1064         NSMutableString *sourcePrivateKey = [[[NSString stringWithContentsOfUTF8File:inPath] mutableCopy] autorelease];
1065         AILog(@"Upgrading private keys at %@ gave %@",inPath,sourcePrivateKey);
1066         if (!sourcePrivateKey || ![sourcePrivateKey length]) return nil;
1068         /*
1069          * Gaim used the account name for the name and the prpl id for the protocol.
1070          * We will use the internalObjectID for the name and the service's uniqueID for the protocol.
1071          */
1073         /* Remove Jabber resources... from the private key list
1074          * If you used a non-default resource, no upgrade for you.
1075          */
1076         [sourcePrivateKey replaceOccurrencesOfString:@"/Adium"
1077                                                                           withString:@""
1078                                                                                  options:NSLiteralSearch
1079                                                                                    range:NSMakeRange(0, [sourcePrivateKey length])];
1081         AIAccount               *account;
1082         NSEnumerator    *enumerator = [[[adium accountController] accounts] objectEnumerator];
1083         NSDictionary    *prplDict = [self prplDict];
1085         while ((account = [enumerator nextObject])) {
1086                 //Hit every possibile name for this account along the way
1087                 NSEnumerator    *accountNameEnumerator = [[NSSet setWithObjects:[account UID],[account formattedUID],[[account UID] compactedString], nil] objectEnumerator];
1088                 NSString                *accountName;
1089                 NSString                *accountInternalObjectID = [NSString stringWithFormat:@"\"%@\"",[account internalObjectID]];
1091                 while ((accountName = [accountNameEnumerator nextObject])) {
1092                         NSRange                 accountNameRange = NSMakeRange(0, 0);
1093                         NSRange                 searchRange = NSMakeRange(0, [sourcePrivateKey length]);
1095                         while (accountNameRange.location != NSNotFound &&
1096                                    (NSMaxRange(searchRange) <= [sourcePrivateKey length])) {
1097                                 //Find the next place this account name is located
1098                                 accountNameRange = [sourcePrivateKey rangeOfString:accountName
1099                                                                                                                    options:NSLiteralSearch
1100                                                                                                                          range:searchRange];
1102                                 if (accountNameRange.location != NSNotFound) {
1103                                         //Update our search range
1104                                         searchRange.location = NSMaxRange(accountNameRange);
1105                                         searchRange.length = [sourcePrivateKey length] - searchRange.location;
1107                                         //Make sure that this account name actually begins and finishes a name; otherwise (name TekJew2) matches (name TekJew)
1108                                         if ((![[sourcePrivateKey substringWithRange:NSMakeRange(accountNameRange.location - 6, 6)] isEqualToString:@"(name "] &&
1109                                                  ![[sourcePrivateKey substringWithRange:NSMakeRange(accountNameRange.location - 7, 7)] isEqualToString:@"(name \""]) ||
1110                                                 (![[sourcePrivateKey substringWithRange:NSMakeRange(NSMaxRange(accountNameRange), 1)] isEqualToString:@")"] &&
1111                                                  ![[sourcePrivateKey substringWithRange:NSMakeRange(NSMaxRange(accountNameRange), 2)] isEqualToString:@"\")"])) {
1112                                                 continue;
1113                                         }
1115                                         /* Within that range, find the next "(protocol " which encloses
1116                                                 * a string of the form "(protocol protocol-name)"
1117                                                 */
1118                                         NSRange protocolRange = [sourcePrivateKey rangeOfString:@"(protocol "
1119                                                                                                                                         options:NSLiteralSearch
1120                                                                                                                                           range:searchRange];
1121                                         if (protocolRange.location != NSNotFound) {
1122                                                 //Update our search range
1123                                                 searchRange.location = NSMaxRange(protocolRange);
1124                                                 searchRange.length = [sourcePrivateKey length] - searchRange.location;
1126                                                 NSRange nextClosingParen = [sourcePrivateKey rangeOfString:@")"
1127                                                                                                                                                    options:NSLiteralSearch
1128                                                                                                                                                          range:searchRange];
1129                                                 NSRange protocolNameRange = NSMakeRange(NSMaxRange(protocolRange),
1130                                                                                                                                 nextClosingParen.location - NSMaxRange(protocolRange));
1131                                                 NSString *protocolName = [sourcePrivateKey substringWithRange:protocolNameRange];
1132                                                 //Remove a trailing quote if necessary
1133                                                 if ([[protocolName substringFromIndex:([protocolName length]-1)] isEqualToString:@"\""]) {
1134                                                         protocolName = [protocolName substringToIndex:([protocolName length]-1)];
1135                                                 }
1137                                                 NSString *uniqueServiceID = [prplDict objectForKey:protocolName];
1139                                                 if ([[[account service] serviceCodeUniqueID] isEqualToString:uniqueServiceID]) {
1140                                                         //Replace the protocol name first
1141                                                         [sourcePrivateKey replaceCharactersInRange:protocolNameRange
1142                                                                                                                         withString:uniqueServiceID];
1144                                                         //Then replace the account name which was before it (so the range hasn't changed)
1145                                                         if ([sourcePrivateKey characterAtIndex:(accountNameRange.location - 1)] == '\"') {
1146                                                                 accountNameRange.location -= 1;
1147                                                                 accountNameRange.length += 1;
1148                                                         }
1149                                                         
1150                                                         if ([sourcePrivateKey characterAtIndex:(accountNameRange.location + accountNameRange.length + 1)] == '\"') {
1151                                                                 accountNameRange.length += 1;
1152                                                         }
1153                                                         
1154                                                         [sourcePrivateKey replaceCharactersInRange:accountNameRange
1155                                                                                                                         withString:accountInternalObjectID];
1156                                                 }
1157                                         }
1158                                 }
1159                                 
1160                                 AILog(@"%@ - %@",accountName, sourcePrivateKey);
1161                         }
1162                 }                       
1163         }
1164         
1165         return sourcePrivateKey;
1168 - (void)upgradeOTRIfNeeded
1170         if (![[[adium preferenceController] preferenceForKey:@"GaimOTR_to_AdiumOTR_Update"
1171                                                                                                    group:@"OTR"] boolValue]) {
1172                 NSString          *destinationPath = [[adium loginController] userDirectory];
1173                 NSString          *sourcePath = [destinationPath stringByAppendingPathComponent:@"libpurple"];
1174                 
1175                 NSString *privateKey = [self upgradedPrivateKeyFromFile:[sourcePath stringByAppendingPathComponent:@"otr.private_key"]];
1176                 if (privateKey && [privateKey length]) {
1177                         [privateKey writeToFile:[destinationPath stringByAppendingPathComponent:@"otr.private_key"]
1178                                                  atomically:NO
1179                                                    encoding:NSUTF8StringEncoding
1180                                                           error:NULL];
1181                 }
1183                 NSString *fingerprints = [self upgradedFingerprintsFromFile:[sourcePath stringByAppendingPathComponent:@"otr.fingerprints"]];
1184                 if (fingerprints && [fingerprints length]) {
1185                         [fingerprints writeToFile:[destinationPath stringByAppendingPathComponent:@"otr.fingerprints"]
1186                                                    atomically:NO
1187                                                          encoding:NSUTF8StringEncoding
1188                                                                 error:NULL];
1189                 }
1191                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
1192                                                                                          forKey:@"GaimOTR_to_AdiumOTR_Update"
1193                                                                                           group:@"OTR"];
1194         }
1195         
1196         if (![[[adium preferenceController] preferenceForKey:@"Libgaim_to_Libpurple_Update"
1197                                                                                                    group:@"OTR"] boolValue]) {
1198                 NSString        *destinationPath = [[adium loginController] userDirectory];
1199                 
1200                 NSString        *privateKeyPath = [destinationPath stringByAppendingPathComponent:@"otr.private_key"];
1201                 NSString        *fingerprintsPath = [destinationPath stringByAppendingPathComponent:@"otr.fingerprints"];
1203                 NSMutableString *privateKeys = [[NSString stringWithContentsOfUTF8File:privateKeyPath] mutableCopy];
1204                 [privateKeys replaceOccurrencesOfString:@"libgaim"
1205                                                                          withString:@"libpurple"
1206                                                                                 options:NSLiteralSearch
1207                                                                                   range:NSMakeRange(0, [privateKeys length])];
1208                 [privateKeys writeToFile:privateKeyPath
1209                                           atomically:YES
1210                                                 encoding:NSUTF8StringEncoding
1211                                                    error:NULL];
1212                 [privateKeys release];
1214                 NSMutableString *fingerprints = [[NSString stringWithContentsOfUTF8File:fingerprintsPath] mutableCopy];
1215                 [fingerprints replaceOccurrencesOfString:@"libgaim"
1216                                                                          withString:@"libpurple"
1217                                                                                 options:NSLiteralSearch
1218                                                                                   range:NSMakeRange(0, [fingerprints length])];
1219                 [fingerprints writeToFile:fingerprintsPath
1220                                            atomically:YES
1221                                                  encoding:NSUTF8StringEncoding
1222                                                         error:NULL];
1223                 [fingerprints release];
1225                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
1226                                                                                          forKey:@"Libgaim_to_Libpurple_Update"
1227                                                                                           group:@"OTR"];
1228         }
1231 @end