2 // AdiumOTREncryption.m
5 // Created by Evan Schoenberg on 12/28/05.
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"
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;
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);
74 if (adiumOTREncryption) {
77 return [adiumOTREncryption retain];
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
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);
99 - (void)adiumFinishedLaunching:(NSNotification *)inNotification
101 [self prepareEncryption];
104 - (void)prepareEncryption
106 /* Initialize the OTR library */
109 [self upgradeOTRIfNeeded];
111 /* Make our OtrlUserState; we'll only use the one. */
112 otrg_plugin_userstate = otrl_userstate_create();
116 err = otrl_privkey_read(otrg_plugin_userstate, PRIVKEY_PATH);
118 const char *errMsg = gpg_strerror(err);
120 if (errMsg && strcmp(errMsg, "No such file or directory")) {
121 NSLog(@"Error reading %s: %s", PRIVKEY_PATH, errMsg);
125 otrg_ui_update_keylist();
127 err = otrl_privkey_read_fingerprints(otrg_plugin_userstate, STORE_PATH,
130 const char *errMsg = gpg_strerror(err);
132 if (errMsg && strcmp(errMsg, "No such file or directory")) {
133 NSLog(@"Error reading %s: %s", STORE_PATH, errMsg);
137 otrg_ui_update_fingerprint();
140 [[adium notificationCenter] addObserver:self
141 selector:@selector(adiumWillTerminate:)
142 name:AIAppWillTerminateNotification
145 [[adium notificationCenter] addObserver:self
146 selector:@selector(updateSecurityDetails:)
147 name:Chat_SourceChanged
149 [[adium notificationCenter] addObserver:self
150 selector:@selector(updateSecurityDetails:)
151 name:Chat_DestinationChanged
153 [[adium notificationCenter] addObserver:self
154 selector:@selector(updateSecurityDetails:)
158 //Add the Encryption preferences
159 OTRPrefs = [[ESOTRPreferences preferencePane] retain];
165 [[adium notificationCenter] removeObserver:self];
174 * @brief Return an NSDictionary* describing a ConnContext.
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
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;
203 case TRUST_NOT_PRIVATE:
204 encryptionStatus = EncryptionStatus_None;
206 case TRUST_UNVERIFIED:
207 encryptionStatus = EncryptionStatus_Unverified;
210 encryptionStatus = EncryptionStatus_Verified;
213 encryptionStatus = EncryptionStatus_Finished;
217 char our_hash[45], their_hash[45];
219 otrl_privkey_fingerprint(otrg_get_userstate(), our_hash,
220 context->accountname, context->protocol);
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"),
246 AILog(@"Security details: %@",securityDetailsDict);
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];
278 chat = [[[AIObject sharedAdiumInstance] chatController] chatWithContact:listContact];
285 static OtrlPolicy policyForContact(AIListContact *contact)
287 OtrlPolicy policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
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;
294 NSNumber *prefNumber;
295 AIEncryptedChatPreference pref;
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];
307 pref = [prefNumber intValue];
310 case EncryptedChat_Never:
311 policy = OTRL_POLICY_NEVER;
313 case EncryptedChat_Manually:
314 case EncryptedChat_Default:
315 policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
317 case EncryptedChat_Automatically:
318 policy = OTRL_POLICY_OPPORTUNISTIC;
320 case EncryptedChat_RejectUnencryptedMessages:
321 policy = OTRL_POLICY_ALWAYS;
325 policy = OTRL_POLICY_MANUAL_AND_REPOND_TO_WHITESPACE;
333 //Return the ConnContext for a Conversation, or NULL if none exists
334 static ConnContext* contextForChat(AIChat *chat)
337 const char *username, *accountname, *proto;
338 ConnContext *context;
340 /* Do nothing if this isn't an IM conversation */
341 if ([chat isGroupChat]) return nil;
343 account = [chat account];
344 accountname = [[account internalObjectID] UTF8String];
345 proto = [[[account service] serviceCodeUniqueID] UTF8String];
346 username = [[[chat listObject] UID] UTF8String];
348 context = otrl_context_find(otrg_plugin_userstate,
349 username, accountname, proto, 0, NULL,
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;
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;
365 level = TRUST_UNVERIFIED;
367 } else if (context && context->msgstate == OTRL_MSGSTATE_FINISHED) {
368 level = TRUST_FINISHED;
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)
385 AIAccount *account = accountFromAccountID(accountname);
386 AIService *service = serviceFromServiceID(protocol);
388 NSString *identifier = [NSString stringWithFormat:@"%@ (%@)",[account formattedUID], [service shortDescription]];
390 [ESOTRPrivateKeyGenerationWindowController startedGeneratingForIdentifier:identifier];
392 /* Generate the key */
393 otrl_privkey_generate(otrg_plugin_userstate, PRIVKEY_PATH,
394 accountname, protocol);
395 otrg_ui_update_keylist();
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)
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
427 static int display_otr_message(const char *accountname, const char *protocol,
428 const char *username, const char *msg)
431 NSObject<AIAdium> *sharedAdium = [AIObject sharedAdiumInstance];
432 AIListContact *listContact = contactFromInfo(accountname, protocol, username);
434 AIContentMessage *messageObject;
436 //We couldn't determine a listContact, so return that we didn't handle the message
437 if (!listContact) return 1;
439 chat = [[sharedAdium chatController] existingChatWithContact:listContact];
441 message = [NSString stringWithUTF8String:msg];
442 AILog(@"display_otr_message: %s %s %s: %s",accountname,protocol,username, msg);
444 if (([message rangeOfString:@"<b>The following message received from"].location != NSNotFound) &&
445 ([message rangeOfString:@"was <i>not</i> encrypted: ["].location != NSNotFound)) {
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
450 NSRange endRange = [message rangeOfString:@"was <i>not</i> encrypted: ["];
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>
455 NSString *OTRMessage = [adiumOTREncryption localizedOTRMessage:@"The following message was <b>not encrypted</b>: "
457 isWorthOpeningANewChat:NULL];
458 message = [OTRMessage stringByAppendingString:
459 [message substringWithRange:NSMakeRange(NSMaxRange(endRange),
460 ([message length] - NSMaxRange(endRange) - [@"<b>]</b>" length]))]];
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]
469 message:[AIHTMLDecoder decodeHTML:message]
472 [[sharedAdium contentController] receiveContentObject:messageObject];
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
486 chat = [[sharedAdium chatController] chatWithContact:listContact];
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.
491 if (![chat isOpen]) chat = nil;
495 [[sharedAdium contentController] displayEvent:[[AIHTMLDecoder decodeHTML:message] string]
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;
563 context = otrl_context_find(us, username, accountname,
564 protocol, 0, NULL, NULL, NULL);
566 if (context == NULL/* || context->msgstate != OTRL_MSGSTATE_ENCRYPTED*/) {
567 NSLog(@"otrg_adium_dialog_unknown_fingerprint: Ack!");
571 [adiumOTREncryption performSelector:@selector(verifyUnknownFingerprint:)
572 withObject:[NSValue valueWithPointer:context]
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)
600 // otrg_dialog_stillconnected(context);
601 AILog(@"Still secure...");
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 = {
616 display_otr_message_cb,
617 update_context_list_cb,
618 account_display_name_cb,
619 account_display_name_free_cb,
621 protocol_name_free_cb,
622 confirm_fingerprint_cb,
623 write_fingerprints_cb,
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;
643 if (!username || !originalMessage)
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);
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;
670 const char *username = [[inListContact UID] UTF8String];
671 const char *accountname = [[inAccount internalObjectID] UTF8String];
672 const char *protocol = [[[inAccount service] serviceCodeUniqueID] UTF8String];
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
679 res = otrl_message_receiving(otrg_plugin_userstate, &ui_ops, NULL,
680 accountname, protocol, username, message,
681 &newMessage, &tlvs, NULL, NULL);
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;
696 otrl_message_free(newMessage);
698 tlv = otrl_tlv_find(tlvs, OTRL_TLV_DISCONNECTED);
700 /* Notify the user that the other side disconnected. */
701 display_otr_message(accountname, protocol, username, CLOSED_CONNECTION_MESSAGE);
703 otrg_ui_update_keylist();
708 return decryptedMessage;
711 - (void)requestSecureOTRMessaging:(BOOL)inSecureMessaging inChat:(AIChat *)inChat
713 if (inSecureMessaging) {
714 send_default_query_to_chat(inChat);
717 disconnect_from_chat(inChat);
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
734 - (void)adiumWillTerminate:(NSNotification *)inNotification
736 ConnContext *context = otrg_plugin_userstate->context_root;
738 ConnContext *next = context->next;
739 if (context->msgstate == OTRL_MSGSTATE_ENCRYPTED &&
740 context->protocol_version > 1) {
741 disconnect_from_context(context);
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
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)
766 - (void)setSecurityDetails:(NSDictionary *)securityDetailsDict forChat:(AIChat *)inChat
769 NSMutableDictionary *fullSecurityDetailsDict;
771 if (securityDetailsDict) {
772 NSString *format, *description;
773 fullSecurityDetailsDict = [[securityDetailsDict mutableCopy] autorelease];
775 /* Encrypted by Off-the-Record Messaging
777 * Fingerprint for TekJew:
780 * Secure ID for this session:
781 * Incoming: <Incoming SessionID>
782 * Outgoing: <Outgoing SessionID>
784 format = [@"%@\n\n" stringByAppendingString:AILocalizedString(@"Fingerprint for %@:","Fingerprint for <name>:")];
785 format = [format stringByAppendingString:@"\n%@\n\n%@\n%@ %@\n%@ %@"];
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"],
798 [fullSecurityDetailsDict setObject:description
799 forKey:@"Description"];
801 fullSecurityDetailsDict = nil;
804 [inChat setSecurityDetails:fullSecurityDetailsDict];
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]));
816 [[[AIObject sharedAdiumInstance] contentController] sendRawMessage:[NSString stringWithUTF8String:(msg ? msg : "?OTRv2?")]
817 toContact:[inChat listObject]];
822 /* Disconnect a context, sending a notice to the other side, if
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));
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;
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;
876 - (void)verifyUnknownFingerprint:(NSValue *)contextValue
878 NSDictionary *responseInfo;
880 responseInfo = details_for_context([contextValue pointerValue]);
882 [ESOTRUnknownFingerprintController showUnknownFingerprintPromptWithResponseInfo:responseInfo];
886 * @brief Call this function when our DSA key is updated; it will redraw the Encryption preferences item, if visible.
888 - (void)prefsShouldUpdatePrivateKeyList
890 [OTRPrefs updatePrivateKeyList];
894 * @brief Update the list of other users' fingerprints, if it's visible
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.
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."),
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."),
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."),
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."),
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),
953 if (isWorthOpeningANewChat) *isWorthOpeningANewChat = YES;
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
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
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];
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"];
1002 [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"\""]];
1004 NSDictionary *prplDict = [self prplDict];
1006 NSArray *adiumAccounts = [[adium accountController] accounts];
1008 while (![scanner isAtEnd]) {
1009 //username accountname protocol key trusted\n
1011 NSString *username = nil, *accountname = nil, *protocol = nil, *key = nil, *trusted = nil;
1014 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&username];
1015 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1018 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&accountname];
1019 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1022 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&protocol];
1023 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1026 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&key];
1027 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:&chunk];
1029 //We have a trusted entry
1030 if ([chunk isEqualToString:@"\t"]) {
1032 [scanner scanUpToCharactersFromSet:tabAndNewlineSet intoString:&trusted];
1033 [scanner scanCharactersFromSet:tabAndNewlineSet intoString:NULL];
1038 if (username && accountname && protocol && key) {
1040 NSEnumerator *enumerator = [adiumAccounts objectEnumerator];
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]];
1049 [outFingerprints appendString:@"\t"];
1050 [outFingerprints appendString:trusted];
1052 [outFingerprints appendString:@"\n"];
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;
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.
1073 /* Remove Jabber resources... from the private key list
1074 * If you used a non-default resource, no upgrade for you.
1076 [sourcePrivateKey replaceOccurrencesOfString:@"/Adium"
1078 options:NSLiteralSearch
1079 range:NSMakeRange(0, [sourcePrivateKey length])];
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
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:@"\")"])) {
1115 /* Within that range, find the next "(protocol " which encloses
1116 * a string of the form "(protocol protocol-name)"
1118 NSRange protocolRange = [sourcePrivateKey rangeOfString:@"(protocol "
1119 options:NSLiteralSearch
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
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)];
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;
1150 if ([sourcePrivateKey characterAtIndex:(accountNameRange.location + accountNameRange.length + 1)] == '\"') {
1151 accountNameRange.length += 1;
1154 [sourcePrivateKey replaceCharactersInRange:accountNameRange
1155 withString:accountInternalObjectID];
1160 AILog(@"%@ - %@",accountName, sourcePrivateKey);
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"];
1175 NSString *privateKey = [self upgradedPrivateKeyFromFile:[sourcePath stringByAppendingPathComponent:@"otr.private_key"]];
1176 if (privateKey && [privateKey length]) {
1177 [privateKey writeToFile:[destinationPath stringByAppendingPathComponent:@"otr.private_key"]
1179 encoding:NSUTF8StringEncoding
1183 NSString *fingerprints = [self upgradedFingerprintsFromFile:[sourcePath stringByAppendingPathComponent:@"otr.fingerprints"]];
1184 if (fingerprints && [fingerprints length]) {
1185 [fingerprints writeToFile:[destinationPath stringByAppendingPathComponent:@"otr.fingerprints"]
1187 encoding:NSUTF8StringEncoding
1191 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
1192 forKey:@"GaimOTR_to_AdiumOTR_Update"
1196 if (![[[adium preferenceController] preferenceForKey:@"Libgaim_to_Libpurple_Update"
1197 group:@"OTR"] boolValue]) {
1198 NSString *destinationPath = [[adium loginController] userDirectory];
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
1210 encoding:NSUTF8StringEncoding
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
1221 encoding:NSUTF8StringEncoding
1223 [fingerprints release];
1225 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
1226 forKey:@"Libgaim_to_Libpurple_Update"