mirror of
https://github.com/Codeux-Software/Textual.git
synced 2026-06-16 13:24:32 +00:00
869 lines
29 KiB
Objective-C
869 lines
29 KiB
Objective-C
/* *********************************************************************
|
|
* _____ _ _
|
|
* |_ _|____ _| |_ _ _ __ _| |
|
|
* | |/ _ \ \/ / __| | | |/ _` | |
|
|
* | | __/> <| |_| |_| | (_| | |
|
|
* |_|\___/_/\_\\__|\__,_|\__,_|_|
|
|
*
|
|
* Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors.
|
|
* Please see Acknowledgements.pdf for additional information.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of Textual, "Codeux Software, LLC", nor the
|
|
* names of its contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*
|
|
*********************************************************************** */
|
|
|
|
#import "TXMasterController.h"
|
|
#import "TXMenuController.h"
|
|
#import "TDCAlert.h"
|
|
#import "TLOLocalization.h"
|
|
#import "TVCMainWindow.h"
|
|
#import "TVCMainWindowTitlebarAccessoryViewPrivate.h"
|
|
#import "TVCLogRenderer.h"
|
|
#import "TPCPathInfoPrivate.h"
|
|
#import "TPCPreferencesLocal.h"
|
|
#import "IRCClientPrivate.h"
|
|
#import "IRCChannelPrivate.h"
|
|
#import "IRCWorld.h"
|
|
#import "TLOEncryptionManagerPrivate.h"
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
|
|
@interface TLOEncryptionManager () <OTRKitDelegate, OTRKitFingerprintManagerDialogDelegate>
|
|
@property (nonatomic, strong, nullable) OTRKitFingerprintManagerDialog *fingerprintManagerDialog;
|
|
@end
|
|
|
|
@interface TLOEncryptionManagerEncodingDecodingObject : NSObject
|
|
// Properties that should be manipulated to provide context information
|
|
@property (nonatomic, copy, nullable) TLOEncryptionManagerEncodingDecodingCallbackBlock encodingCallback;
|
|
@property (nonatomic, copy, nullable) TLOEncryptionManagerInjectCallbackBlock injectionCallback;
|
|
@property (nonatomic, copy) NSString *messageFrom;
|
|
@property (nonatomic, copy) NSString *messageTo;
|
|
@property (nonatomic, copy) NSString *messageBody; // unencrypted value
|
|
@end
|
|
|
|
@implementation TLOEncryptionManager
|
|
|
|
#pragma mark -
|
|
#pragma mark Initialization
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
[self prepareInitialState];
|
|
|
|
return self;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)prepareInitialState
|
|
{
|
|
[self setupEncryptionManager];
|
|
}
|
|
|
|
- (nullable NSString *)pathToStoreEncryptionSecrets
|
|
{
|
|
NSString *sourcePath = [TPCPathInfo groupContainerApplicationSupport];
|
|
|
|
if (sourcePath == nil) {
|
|
return nil;
|
|
}
|
|
|
|
NSString *basePath = [sourcePath stringByAppendingPathComponent:@"/Encryption Components/"];
|
|
|
|
[TPCPathInfo _createDirectoryAtPath:basePath];
|
|
|
|
return basePath;
|
|
}
|
|
|
|
- (void)setupEncryptionManager
|
|
{
|
|
OTRKit *otrKit = [OTRKit sharedInstance];
|
|
|
|
otrKit.accountNameSeparator = @"@";
|
|
|
|
otrKit.delegate = (id)self;
|
|
|
|
[otrKit setupWithDataPath:[self pathToStoreEncryptionSecrets]];
|
|
|
|
[self prepareEncryptionComponentPath:otrKit.fingerprintsPath];
|
|
[self prepareEncryptionComponentPath:otrKit.instanceTagsPath];
|
|
[self prepareEncryptionComponentPath:otrKit.privateKeyPath];
|
|
|
|
NSURL *componentPathURL = [NSURL fileURLWithPath:otrKit.dataPath isDirectory:YES];
|
|
|
|
NSError *attributesChangeError = nil;
|
|
|
|
if ([componentPathURL setResourceValue:@(YES) forKey:NSURLIsHiddenKey error:&attributesChangeError] == NO) {
|
|
LogToConsoleError("Failed to hide the folder at the path '%{private}@': %{private}@",
|
|
componentPathURL.standardizedTildePath, attributesChangeError.localizedDescription);
|
|
}
|
|
|
|
[otrKit setMaximumProtocolSize:[self otrKitProtocolMaximumMessageSize]
|
|
forProtocol:[self otrKitProtocol]];
|
|
|
|
[self updatePolicy];
|
|
}
|
|
|
|
- (void)prepareEncryptionComponentPath:(NSString *)path
|
|
{
|
|
NSParameterAssert(path != nil);
|
|
|
|
/* Create the path if it does not already exist. */
|
|
if ([RZFileManager() fileExistsAtPath:path] == NO) {
|
|
NSError *writeError = nil;
|
|
|
|
if ([@"" writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&writeError] == NO) {
|
|
LogToConsoleError("Failed to create base file for encryption component at path: %{private}@",
|
|
writeError.localizedDescription);
|
|
}
|
|
}
|
|
|
|
/* Files are stored in a location that is accessible to Time Machine
|
|
which means we must mark the files to not be backed up. */
|
|
NSURL *pathURL = [NSURL fileURLWithPath:path isDirectory:NO];
|
|
|
|
NSError *attributesChangeError = nil;
|
|
|
|
if ([pathURL setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:&attributesChangeError] == NO) {
|
|
LogToConsoleError("Failed to exclude the files at the path '%{private}@' from backup: %{private}@",
|
|
pathURL.standardizedTildePath, attributesChangeError.localizedDescription);
|
|
}
|
|
}
|
|
|
|
- (void)prepareForApplicationTermination
|
|
{
|
|
LogToConsoleTerminationProgress("Preparing encryption manager");
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Fingerprint Manager
|
|
|
|
- (void)presentListOfFingerprints
|
|
{
|
|
if (self.fingerprintManagerDialog == nil) {
|
|
OTRKitFingerprintManagerDialog *dialog = [OTRKitFingerprintManagerDialog new];
|
|
|
|
dialog.delegate = (id)self;
|
|
|
|
self.fingerprintManagerDialog = dialog;
|
|
}
|
|
|
|
[self.fingerprintManagerDialog open:mainWindow()];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Account Name Information
|
|
|
|
- (NSString *)accountNameForUser:(NSString *)nickname onClient:(IRCClient *)client
|
|
{
|
|
NSParameterAssert(nickname != nil);
|
|
NSParameterAssert(client != nil);
|
|
|
|
return [NSString stringWithFormat:@"%@%@%@", nickname, [OTRKit sharedInstance].accountNameSeparator, client.uniqueIdentifier];
|
|
}
|
|
|
|
- (nullable NSString *)nicknameFromAccountName:(NSString *)accountName
|
|
{
|
|
NSParameterAssert(accountName != nil);
|
|
|
|
NSString *nickname = [[OTRKit sharedInstance] leftPortionOfAccountName:accountName];
|
|
|
|
return nickname;
|
|
}
|
|
|
|
- (nullable IRCClient *)connectionFromAccountName:(NSString *)accountName
|
|
{
|
|
NSParameterAssert(accountName != nil);
|
|
|
|
NSString *clientIdentifier = [[OTRKit sharedInstance] rightPortionOfAccountName:accountName];
|
|
|
|
return [worldController() findClientWithId:clientIdentifier];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Starting Encryption & Stopping Encryption
|
|
|
|
- (void)beginConversationWith:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
[self refreshConversationWith:messageTo from:messageFrom presentMessage:TXTLS(@"OffTheRecord[aii-2q]")];
|
|
}
|
|
|
|
- (void)endConversationWith:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
|
|
OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (currentState == OTRKitMessageStateEncrypted) {
|
|
[[OTRKit sharedInstance] disableEncryptionWithUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
} else {
|
|
[self presentErrorMessage:TXTLS(@"OffTheRecord[g9p-8r]") withAccountName:messageTo];
|
|
}
|
|
}
|
|
|
|
- (void)refreshConversationWith:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
[self refreshConversationWith:messageTo from:messageFrom presentMessage:TXTLS(@"OffTheRecord[f59-3b]")];
|
|
}
|
|
|
|
- (void)refreshConversationWith:(NSString *)messageTo from:(NSString *)messageFrom presentMessage:(NSString *)message
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
NSParameterAssert(message != nil);
|
|
|
|
[self presentMessage:message withAccountName:messageTo];
|
|
|
|
OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (currentState == OTRKitMessageStateEncrypted) {
|
|
[[OTRKit sharedInstance] disableEncryptionWithUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
}
|
|
|
|
[[OTRKit sharedInstance] initiateEncryptionWithUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]
|
|
asynchronously:YES];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Socialist Millionaire
|
|
|
|
- (void)authenticateUser:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
|
|
OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (currentState == OTRKitMessageStateEncrypted) {
|
|
[OTRKitAuthenticationDialog requestAuthenticationForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
} else {
|
|
[self presentErrorMessage:TXTLS(@"OffTheRecord[jrv-gq]") withAccountName:messageTo];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Encryption & Decryption
|
|
|
|
- (void)decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom to:(NSString *)messageTo decodingCallback:(nullable TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
NSParameterAssert(messageBody != nil);
|
|
|
|
TLOEncryptionManagerEncodingDecodingObject *messageObject = [TLOEncryptionManagerEncodingDecodingObject new];
|
|
|
|
messageObject.messageTo = messageTo;
|
|
messageObject.messageFrom = messageFrom;
|
|
|
|
messageObject.messageBody = messageBody;
|
|
|
|
messageObject.encodingCallback = decodingCallback;
|
|
|
|
/* Messages are decoded synchronously because many things incoming
|
|
from IRC are never passed through OTRKit. To keep the incoming
|
|
queue synchronous, we keep it synchronous here. */
|
|
[[OTRKit sharedInstance] decodeMessage:messageBody
|
|
username:messageFrom
|
|
accountName:messageTo
|
|
protocol:[self otrKitProtocol]
|
|
asynchronously:NO
|
|
tag:messageObject];
|
|
}
|
|
|
|
- (void)encryptMessage:(NSString *)messageBody from:(NSString *)messageFrom to:(NSString *)messageTo encodingCallback:(nullable TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(nullable TLOEncryptionManagerInjectCallbackBlock)injectionCallback
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
NSParameterAssert(messageBody != nil);
|
|
|
|
TLOEncryptionManagerEncodingDecodingObject *messageObject = [TLOEncryptionManagerEncodingDecodingObject new];
|
|
|
|
messageObject.messageTo = messageTo;
|
|
messageObject.messageFrom = messageFrom;
|
|
|
|
messageObject.messageBody = messageBody;
|
|
|
|
messageObject.encodingCallback = encodingCallback;
|
|
|
|
messageObject.injectionCallback = injectionCallback;
|
|
|
|
[[OTRKit sharedInstance] encodeMessage:messageBody
|
|
tlvs:nil
|
|
username:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]
|
|
asynchronously:NO
|
|
tag:messageObject];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Helper Methods
|
|
|
|
- (OTRKitMessageState)messageStateFor:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
|
|
OTRKitMessageState currentState =
|
|
[[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
return currentState;
|
|
}
|
|
|
|
- (BOOL)safeToTransferFile:(NSString *)filename to:(NSString *)messageTo from:(NSString *)messageFrom isIncomingFileTransfer:(BOOL)isIncomingFileTransfer
|
|
{
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
|
|
OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (currentState == OTRKitMessageStateEncrypted) {
|
|
if (isIncomingFileTransfer) {
|
|
BOOL continueop = [TDCAlert modalAlertWithMessage:TXTLS(@"OffTheRecord[nuo-8s]")
|
|
title:TXTLS(@"OffTheRecord[mnw-qt]", filename)
|
|
defaultButton:TXTLS(@"Prompts[qso-2g]")
|
|
alternateButton:TXTLS(@"OffTheRecord[ng0-5q]")];
|
|
|
|
return (continueop == NO);
|
|
}
|
|
else
|
|
{
|
|
NSString *nickname = [self nicknameFromAccountName:messageTo];
|
|
|
|
BOOL continueop = [TDCAlert modalAlertWithMessage:TXTLS(@"OffTheRecord[7he-76]")
|
|
title:TXTLS(@"OffTheRecord[gcg-tv]", filename, nickname)
|
|
defaultButton:TXTLS(@"Prompts[qso-2g]")
|
|
alternateButton:TXTLS(@"OffTheRecord[0v0-ix]")];
|
|
|
|
return (continueop == NO);
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)updateLockIconButton:(TVCMainWindowTitlebarAccessoryViewLockButton *)button withStateOf:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
NSParameterAssert(button != nil);
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
|
|
OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (currentState == OTRKitMessageStateEncrypted) {
|
|
BOOL hasVerifiedKey = [[OTRKit sharedInstance] activeFingerprintIsVerifiedForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (hasVerifiedKey) {
|
|
button.title = TXTLS(@"OffTheRecord[l9n-p9]");
|
|
|
|
[button setIconAsLocked];
|
|
} else {
|
|
button.title = TXTLS(@"OffTheRecord[w34-mg]");
|
|
|
|
/* Even though we are encrypted, our icon is still set to unlocked because
|
|
the identity of messageTo still has not been authenticated. */
|
|
[button setIconAsUnlocked];
|
|
}
|
|
} else {
|
|
button.title = TXTLS(@"OffTheRecord[anu-ky]");
|
|
|
|
[button setIconAsUnlocked];
|
|
}
|
|
}
|
|
|
|
- (void)performBlock:(void (^)(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel))block inRelationToAccountName:(NSString *)accountName
|
|
{
|
|
[self performBlock:block inRelationToAccountName:accountName createWindowIfMissing:NO];
|
|
}
|
|
|
|
- (void)performBlock:(void (^)(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel))block inRelationToAccountName:(NSString *)accountName createWindowIfMissing:(BOOL)createWindowIfMissing
|
|
{
|
|
XRPerformBlockSynchronouslyOnMainQueue(^{
|
|
IRCClient *client = [self connectionFromAccountName:accountName];
|
|
|
|
if (client == nil) {
|
|
return;
|
|
}
|
|
|
|
NSString *nickname = [self nicknameFromAccountName:accountName];
|
|
|
|
IRCChannel *channel = nil;
|
|
|
|
if (createWindowIfMissing) {
|
|
channel = [client findChannelOrCreate:nickname isPrivateMessage:YES];
|
|
} else {
|
|
channel = [client findChannel:nickname];
|
|
}
|
|
|
|
block(nickname, client, channel);
|
|
});
|
|
}
|
|
|
|
- (nullable NSString *)localizedStringForEvent:(OTRKitMessageEvent)event
|
|
{
|
|
NSString *localeKey = nil;
|
|
|
|
#define _dv(event, localInt) case (event): { localeKey = (localInt); break; }
|
|
|
|
switch (event) {
|
|
_dv(OTRKitMessageEventEncryptionRequired, @"jww-e0")
|
|
_dv(OTRKitMessageEventEncryptionError, @"jvt-fi")
|
|
_dv(OTRKitMessageEventConnectionEnded, @"c5o-2l")
|
|
_dv(OTRKitMessageEventSetupError, @"uhy-85")
|
|
_dv(OTRKitMessageEventMessageReflected, @"bl0-5i")
|
|
_dv(OTRKitMessageEventMessageResent, @"c49-q0")
|
|
_dv(OTRKitMessageEventReceivedMessageNotInPrivate, @"6v9-w3")
|
|
_dv(OTRKitMessageEventReceivedMessageUnreadable, @"9if-xp")
|
|
_dv(OTRKitMessageEventReceivedMessageMalformed, @"auo-n0")
|
|
_dv(OTRKitMessageEventLogHeartbeatReceived, @"nl1-nf")
|
|
_dv(OTRKitMessageEventLogHeartbeatSent, @"iwt-9f")
|
|
_dv(OTRKitMessageEventReceivedMessageGeneralError, @"2zx-p6")
|
|
_dv(OTRKitMessageEventReceivedMessageUnencrypted, @"1lf-f0")
|
|
_dv(OTRKitMessageEventReceivedMessageUnrecognized, @"4by-8j")
|
|
_dv(OTRKitMessageEventReceivedMessageForOtherInstance, @"c7t-vi")
|
|
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
#undef _dv
|
|
|
|
if (localeKey) {
|
|
localeKey = [NSString stringWithFormat:@"OffTheRecord[%@]", localeKey];
|
|
|
|
return TXTLS(localeKey);
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (BOOL)eventIsErroneous:(OTRKitMessageEvent)event
|
|
{
|
|
switch (event) {
|
|
case OTRKitMessageEventEncryptionError:
|
|
case OTRKitMessageEventReceivedMessageGeneralError:
|
|
case OTRKitMessageEventReceivedMessageMalformed:
|
|
case OTRKitMessageEventReceivedMessageNotInPrivate:
|
|
case OTRKitMessageEventReceivedMessageUnreadable:
|
|
case OTRKitMessageEventReceivedMessageUnrecognized:
|
|
case OTRKitMessageEventEncryptionRequired:
|
|
{
|
|
return YES;
|
|
}
|
|
default:
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)printMessage:(NSString *)message inChannel:(IRCChannel *)channel onClient:(IRCClient *)client
|
|
{
|
|
[self printMessage:message inChannel:channel onClient:client escapeMessage:YES];
|
|
}
|
|
|
|
- (void)printMessage:(NSString *)message inChannel:(IRCChannel *)channel onClient:(IRCClient *)client escapeMessage:(BOOL)escapeMessage
|
|
{
|
|
NSParameterAssert(message != nil);
|
|
NSParameterAssert(channel != nil);
|
|
NSParameterAssert(client != nil);
|
|
|
|
[client print:message
|
|
by:nil
|
|
inChannel:channel
|
|
asType:TVCLogLineTypeOffTheRecordEncryptionStatus
|
|
command:TVCLogLineDefaultCommandValue
|
|
escapeMessage:escapeMessage];
|
|
}
|
|
|
|
- (void)presentMessage:(NSString *)message withAccountName:(NSString *)accountName
|
|
{
|
|
[self presentMessage:message withAccountName:accountName escapeMessage:YES];
|
|
}
|
|
|
|
- (void)presentMessage:(NSString *)message withAccountName:(NSString *)accountName escapeMessage:(BOOL)escapeMessage
|
|
{
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
if (channel == nil) {
|
|
return;
|
|
}
|
|
|
|
[self printMessage:message inChannel:channel onClient:client escapeMessage:escapeMessage];
|
|
} inRelationToAccountName:accountName
|
|
createWindowIfMissing:YES];
|
|
}
|
|
|
|
- (void)presentErrorMessage:(NSString *)errorMessage withAccountName:(NSString *)accountName
|
|
{
|
|
[self presentErrorMessage:errorMessage withAccountName:accountName escapeMessage:YES];
|
|
}
|
|
|
|
- (void)presentErrorMessage:(NSString *)errorMessage withAccountName:(NSString *)accountName escapeMessage:(BOOL)escapeMessage
|
|
{
|
|
[self presentMessage:errorMessage withAccountName:accountName escapeMessage:escapeMessage];
|
|
}
|
|
|
|
- (void)authenticationStatusChangedForAccountName:(NSString *)accountName isVerified:(BOOL)isVerified
|
|
{
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
if (channel == nil) {
|
|
return;
|
|
}
|
|
|
|
if (isVerified) {
|
|
[self printMessage:TXTLS(@"OffTheRecord[rj0-ys]", nickname) inChannel:channel onClient:client];
|
|
} else {
|
|
[self printMessage:TXTLS(@"OffTheRecord[ufr-mh]", nickname) inChannel:channel onClient:client];
|
|
}
|
|
|
|
[channel noteEncryptionStateDidChange];
|
|
} inRelationToAccountName:accountName];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Off-the-Record Kit Delegate
|
|
|
|
- (void)updatePolicy
|
|
{
|
|
if ([TPCPreferences textEncryptionIsEnabled] == NO) {
|
|
[OTRKit sharedInstance].otrPolicy = OTRKitPolicyNever;
|
|
|
|
return;
|
|
}
|
|
|
|
if ([TPCPreferences textEncryptionIsRequired]) {
|
|
[OTRKit sharedInstance].otrPolicy = OTRKitPolicyAlways;
|
|
} else if ([TPCPreferences textEncryptionIsOpportunistic]) {
|
|
[OTRKit sharedInstance].otrPolicy = OTRKitPolicyOpportunistic;
|
|
} else {
|
|
[OTRKit sharedInstance].otrPolicy = OTRKitPolicyManual;
|
|
}
|
|
}
|
|
|
|
- (NSString *)otrKitProtocol
|
|
{
|
|
return @"prpl-irc";
|
|
}
|
|
|
|
- (int)otrKitProtocolMaximumMessageSize
|
|
{
|
|
return 400; // Chosen by fair dice roll.
|
|
}
|
|
|
|
- (NSString *)maybeInsertProperNegotiationMessage:(NSString *)message
|
|
{
|
|
static NSRegularExpression *boundryRegex = nil;
|
|
|
|
if (boundryRegex == nil) {
|
|
NSString *boundryMatch = [NSString stringWithFormat:
|
|
@"\\?OTRv?([0-9]+)\\?\n<b>(.*)</b> has requested an "
|
|
@"<a href=\"https://otr.cypherpunks.ca/\">Off-the-Record "
|
|
@"private conversation</a>. However, you do not have a plugin "
|
|
@"to support that.\nSee <a href=\"https://otr.cypherpunks.ca/\">"
|
|
@"https://otr.cypherpunks.ca/</a> for more information."];
|
|
|
|
boundryRegex = [NSRegularExpression regularExpressionWithPattern:boundryMatch options:0 error:NULL];
|
|
}
|
|
|
|
NSUInteger numberOfMatches = [boundryRegex numberOfMatchesInString:message options:0 range:message.range];
|
|
|
|
if (numberOfMatches == 1) {
|
|
NSArray *messageComponents = [message componentsSeparatedByString:@"\n"];
|
|
|
|
return [NSString stringWithFormat:@"%@ %@", messageComponents[0], TXTLS(@"OffTheRecord[8hr-8l]")];
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit injectMessage:(NSString *)message username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag
|
|
{
|
|
message = [self maybeInsertProperNegotiationMessage:message];
|
|
|
|
if (tag && [tag isKindOfClass:[TLOEncryptionManagerEncodingDecodingObject class]]) {
|
|
TLOEncryptionManagerEncodingDecodingObject *messageObject = tag;
|
|
|
|
if (messageObject.injectionCallback) {
|
|
messageObject.injectionCallback(message);
|
|
|
|
return; // Do not continue after callback block...
|
|
}
|
|
}
|
|
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
[client send:@"PRIVMSG", nickname, message, nil];
|
|
} inRelationToAccountName:username];
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit encodedMessage:(nullable NSString *)encodedMessage wasEncrypted:(BOOL)wasEncrypted username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag error:(nullable NSError *)error
|
|
{
|
|
if (tag == nil || [tag isKindOfClass:[TLOEncryptionManagerEncodingDecodingObject class]] == NO) {
|
|
return;
|
|
}
|
|
|
|
TLOEncryptionManagerEncodingDecodingObject *messageObject = tag;
|
|
|
|
if (messageObject.encodingCallback) {
|
|
messageObject.encodingCallback(messageObject.messageBody, wasEncrypted);
|
|
}
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit decodedMessage:(nullable NSString *)decodedMessage wasEncrypted:(BOOL)wasEncrypted tlvs:(NSArray<OTRTLV *> *)tlvs username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag
|
|
{
|
|
if (decodedMessage == nil) {
|
|
return;
|
|
}
|
|
|
|
if (tag == nil || [tag isKindOfClass:[TLOEncryptionManagerEncodingDecodingObject class]] == NO) {
|
|
return;
|
|
}
|
|
|
|
TLOEncryptionManagerEncodingDecodingObject *messageObject = tag;
|
|
|
|
if (messageObject.encodingCallback) {
|
|
messageObject.encodingCallback(decodedMessage, wasEncrypted);
|
|
}
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit updateMessageState:(OTRKitMessageState)messageState username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol
|
|
{
|
|
/* We do not force create window if it does not exist when updating encryption
|
|
status because status changes are only important if one is open. When a new
|
|
window is created, it will populate the latest state regardless of delegate. */
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
if (channel == nil) {
|
|
return;
|
|
}
|
|
|
|
[channel noteEncryptionStateDidChange];
|
|
} inRelationToAccountName:username];
|
|
|
|
if (messageState == OTRKitMessageStateEncrypted) {
|
|
BOOL isVerified = [[OTRKit sharedInstance] activeFingerprintIsVerifiedForUsername:username
|
|
accountName:accountName
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
if (isVerified) {
|
|
[self presentMessage:TXTLS(@"OffTheRecord[r3w-fj]") withAccountName:username];
|
|
} else {
|
|
[self presentMessage:TXTLS(@"OffTheRecord[l49-9y]") withAccountName:username escapeMessage:NO];
|
|
}
|
|
} else if (messageState == OTRKitMessageStateFinished ||
|
|
messageState == OTRKitMessageStatePlaintext)
|
|
{
|
|
[self presentMessage:TXTLS(@"OffTheRecord[m1z-eb]") withAccountName:username];
|
|
|
|
/* When policy is changed to never, all open conversations are
|
|
closed. Because user probably wont intend to use authentication
|
|
dialogs without encryption enabled, then let's cancel any that
|
|
are open for this condition. */
|
|
if ([TPCPreferences textEncryptionIsEnabled] == NO) {
|
|
[OTRKitAuthenticationDialog cancelRequestForUsername:username
|
|
accountName:accountName
|
|
protocol:protocol];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)otrKit:(OTRKit *)otrKit isUsernameLoggedIn:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol
|
|
{
|
|
__block BOOL userIsActive = NO;
|
|
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
if (channel == nil) {
|
|
return;
|
|
}
|
|
|
|
userIsActive = channel.isActive;
|
|
} inRelationToAccountName:username];
|
|
|
|
return userIsActive;
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit showFingerprintConfirmationForTheirHash:(NSString *)theirHash ourHash:(NSString *)ourHash username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol
|
|
{
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
/* We print this message unescaped to include an anchor in the HTML
|
|
that the user can click to authenticate the user.
|
|
We are passing outside input to it, which we do escape. */
|
|
[self printMessage:TXTLS(@"OffTheRecord[67n-6j]",
|
|
[TVCLogRenderer escapeHTML:nickname],
|
|
[TVCLogRenderer escapeHTML:theirHash])
|
|
inChannel:channel
|
|
onClient:client
|
|
escapeMessage:NO];
|
|
} inRelationToAccountName:username
|
|
createWindowIfMissing:YES];
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit handleSMPEvent:(OTRKitSMPEvent)event progress:(double)progress question:(nullable NSString *)question username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol error:(nullable NSError *)error
|
|
{
|
|
[OTRKitAuthenticationDialog handleAuthenticationRequest:event progress:progress question:question username:username accountName:accountName protocol:protocol];
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit handleMessageEvent:(OTRKitMessageEvent)event message:(NSString *)message username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag error:(nullable NSError *)error
|
|
{
|
|
if (event == OTRKitMessageEventReceivedMessageUnencrypted) {
|
|
[self otrKit:otrKit decodedMessage:message wasEncrypted:NO tlvs:nil username:username accountName:accountName protocol:protocol tag:tag];
|
|
|
|
return;
|
|
}
|
|
|
|
if ([self eventIsErroneous:event]) {
|
|
NSString *errorMessage = [self localizedStringForEvent:event];
|
|
|
|
[self presentErrorMessage:errorMessage withAccountName:username];
|
|
}
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit receivedSymmetricKey:(NSData *)symmetricKey forUse:(NSUInteger)use useData:(NSData *)useData username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol
|
|
{
|
|
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit willStartGeneratingPrivateKeyForAccountName:(NSString *)accountName protocol:(NSString *)protocol
|
|
{
|
|
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit didFinishGeneratingPrivateKeyForAccountName:(NSString *)accountName protocol:(NSString *)protocol error:(nullable NSError *)error
|
|
{
|
|
|
|
}
|
|
|
|
- (void)otrKit:(OTRKit *)otrKit fingerprintIsVerifiedStateChangedForUsername:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol verified:(BOOL)verified
|
|
{
|
|
[self authenticationStatusChangedForAccountName:username isVerified:verified];
|
|
}
|
|
|
|
- (void)otrKitFingerprintManagerDialogDidClose:(OTRKitFingerprintManagerDialog *)otrkitFingerprintManager
|
|
{
|
|
self.fingerprintManagerDialog = nil;
|
|
}
|
|
|
|
- (BOOL)otrKit:(OTRKit *)otrKit ignoreMessage:(NSString *)message messageType:(OTRKitMessageType)messageType username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol
|
|
{
|
|
__block BOOL ignoreMessage = NO;
|
|
|
|
[self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) {
|
|
if (messageType == OTRKitMessageTypeNotOTR) {
|
|
return;
|
|
}
|
|
|
|
if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage]) {
|
|
ignoreMessage = [client nicknameIsMyself:nickname];
|
|
}
|
|
} inRelationToAccountName:username];
|
|
|
|
return ignoreMessage;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Menu Item Actions
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem withStateOf:(NSString *)messageTo from:(NSString *)messageFrom
|
|
{
|
|
NSParameterAssert(menuItem != nil);
|
|
NSParameterAssert(messageTo != nil);
|
|
NSParameterAssert(messageFrom != nil);
|
|
|
|
NSUInteger menuItemTag = menuItem.tag;
|
|
|
|
if (menuItemTag == MTOTRStatusButtonViewListOfFingerprints) {
|
|
return YES;
|
|
}
|
|
|
|
OTRKitMessageState currentMessageState = [[OTRKit sharedInstance] messageStateForUsername:messageTo
|
|
accountName:messageFrom
|
|
protocol:[self otrKitProtocol]];
|
|
|
|
BOOL messageStateEncrypted = (currentMessageState == OTRKitMessageStateEncrypted);
|
|
|
|
switch (menuItemTag) {
|
|
case MTOTRStatusButtonStartPrivateConversation:
|
|
{
|
|
menuItem.hidden = messageStateEncrypted;
|
|
|
|
return YES;
|
|
}
|
|
case MTOTRStatusButtonRefreshPrivateConversation:
|
|
{
|
|
menuItem.hidden = (messageStateEncrypted == NO);
|
|
|
|
return YES;
|
|
}
|
|
case MTOTRStatusButtonEndPrivateConversation:
|
|
{
|
|
return messageStateEncrypted;
|
|
}
|
|
case MTOTRStatusButtonAuthenticateChatPartner:
|
|
{
|
|
return messageStateEncrypted;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Dummy Class
|
|
|
|
@implementation TLOEncryptionManagerEncodingDecodingObject
|
|
@end
|
|
#endif
|
|
|
|
NS_ASSUME_NONNULL_END
|