mirror of https://github.com/oxen-io/session-ios
147 changed files with 42 additions and 11302 deletions
@ -1,194 +0,0 @@
|
||||
// |
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. |
||||
// |
||||
|
||||
import Foundation |
||||
import PromiseKit |
||||
import SignalUtilitiesKit |
||||
|
||||
@objc(OWSSessionResetJobQueue) |
||||
public class SessionResetJobQueue: NSObject, SignalUtilitiesKit.JobQueue { |
||||
|
||||
@objc(addContactThread:transaction:) |
||||
public func add(contactThread: TSContactThread, transaction: YapDatabaseReadWriteTransaction) { |
||||
let jobRecord = OWSSessionResetJobRecord(contactThread: contactThread, label: self.jobRecordLabel) |
||||
self.add(jobRecord: jobRecord, transaction: transaction) |
||||
} |
||||
|
||||
// MARK: JobQueue |
||||
|
||||
public typealias DurableOperationType = SessionResetOperation |
||||
public let jobRecordLabel: String = "SessionReset" |
||||
public static let maxRetries: UInt = 10 |
||||
public let requiresInternet: Bool = true |
||||
public var runningOperations: [SessionResetOperation] = [] |
||||
|
||||
@objc |
||||
public override init() { |
||||
super.init() |
||||
|
||||
AppReadiness.runNowOrWhenAppWillBecomeReady { |
||||
self.setup() |
||||
} |
||||
} |
||||
|
||||
@objc |
||||
public func setup() { |
||||
defaultSetup() |
||||
} |
||||
|
||||
public var isSetup: Bool = false |
||||
|
||||
public func didMarkAsReady(oldJobRecord: JobRecordType, transaction: YapDatabaseReadWriteTransaction) { |
||||
// no special handling |
||||
} |
||||
|
||||
let operationQueue: OperationQueue = { |
||||
// no need to serialize the operation queuing, since sending will ultimately be serialized by MessageSender |
||||
let operationQueue = OperationQueue() |
||||
operationQueue.name = "SessionReset.OperationQueue" |
||||
return operationQueue |
||||
}() |
||||
|
||||
public func operationQueue(jobRecord: OWSSessionResetJobRecord) -> OperationQueue { |
||||
return self.operationQueue |
||||
} |
||||
|
||||
public func buildOperation(jobRecord: OWSSessionResetJobRecord, transaction: YapDatabaseReadTransaction) throws -> SessionResetOperation { |
||||
guard let contactThread = TSThread.fetch(uniqueId: jobRecord.contactThreadId, transaction: transaction) as? TSContactThread else { |
||||
throw JobError.obsolete(description: "thread for session reset no longer exists") |
||||
} |
||||
|
||||
return SessionResetOperation(contactThread: contactThread, jobRecord: jobRecord) |
||||
} |
||||
} |
||||
|
||||
public class SessionResetOperation: OWSOperation, DurableOperation { |
||||
|
||||
// MARK: DurableOperation |
||||
|
||||
public let jobRecord: OWSSessionResetJobRecord |
||||
|
||||
weak public var durableOperationDelegate: SessionResetJobQueue? |
||||
|
||||
public var operation: OWSOperation { |
||||
return self |
||||
} |
||||
|
||||
// MARK: |
||||
|
||||
let contactThread: TSContactThread |
||||
var recipientId: String { |
||||
return contactThread.contactIdentifier() |
||||
} |
||||
|
||||
@objc public required init(contactThread: TSContactThread, jobRecord: OWSSessionResetJobRecord) { |
||||
self.contactThread = contactThread |
||||
self.jobRecord = jobRecord |
||||
} |
||||
|
||||
// MARK: Dependencies |
||||
|
||||
var dbConnection: YapDatabaseConnection { |
||||
return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection |
||||
} |
||||
|
||||
var primaryStorage: OWSPrimaryStorage { |
||||
return SSKEnvironment.shared.primaryStorage |
||||
} |
||||
|
||||
// MARK: |
||||
|
||||
var firstAttempt = true |
||||
|
||||
override public func run() { |
||||
assert(self.durableOperationDelegate != nil) |
||||
|
||||
/* |
||||
let endSessionMessage = EndSessionMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: self.contactThread) |
||||
|
||||
firstly { |
||||
return self.messageSender.sendPromise(message: endSessionMessage) |
||||
}.done { |
||||
Logger.info("successfully sent EndSessionMessage.") |
||||
Storage.writeSync { transaction in |
||||
// Archive the just-created session since the recipient should delete their corresponding |
||||
// session upon receiving and decrypting our EndSession message. |
||||
// Otherwise if we send another message before them, they wont have the session to decrypt it. |
||||
self.primaryStorage.archiveAllSessions(forContact: self.recipientId, protocolContext: transaction) |
||||
|
||||
/* Loki: Original code |
||||
* ================ |
||||
let message = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), |
||||
in: self.contactThread, |
||||
messageType: TSInfoMessageType.typeSessionDidEnd) |
||||
message.save(with: transaction) |
||||
* ================ |
||||
*/ |
||||
|
||||
if (self.contactThread.sessionResetStatus != .requestReceived) { |
||||
let message = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: self.contactThread, messageType: .typeLokiSessionResetInProgress) |
||||
message.save(with: transaction) |
||||
|
||||
// Loki: We have initiated a session reset |
||||
SNLog("Session reset initiated.") |
||||
self.contactThread.sessionResetStatus = .initiated |
||||
self.contactThread.save(with: transaction) |
||||
} |
||||
} |
||||
self.reportSuccess() |
||||
}.catch { error in |
||||
Logger.error("sending error: \(error.localizedDescription)") |
||||
self.reportError(error) |
||||
}.retainUntilComplete() |
||||
*/ |
||||
} |
||||
|
||||
override public func didSucceed() { |
||||
Storage.writeSync { transaction in |
||||
self.durableOperationDelegate?.durableOperationDidSucceed(self, transaction: transaction) |
||||
} |
||||
} |
||||
|
||||
override public func didReportError(_ error: Error) { |
||||
Logger.debug("remainingRetries: \(self.remainingRetries)") |
||||
|
||||
Storage.writeSync { transaction in |
||||
self.durableOperationDelegate?.durableOperation(self, didReportError: error, transaction: transaction) |
||||
} |
||||
} |
||||
|
||||
override public func retryInterval() -> TimeInterval { |
||||
// Arbitrary backoff factor... |
||||
// With backOffFactor of 1.9 |
||||
// try 1 delay: 0.00s |
||||
// try 2 delay: 0.19s |
||||
// ... |
||||
// try 5 delay: 1.30s |
||||
// ... |
||||
// try 11 delay: 61.31s |
||||
let backoffFactor = 1.9 |
||||
let maxBackoff = kHourInterval |
||||
|
||||
let seconds = 0.1 * min(maxBackoff, pow(backoffFactor, Double(self.jobRecord.failureCount))) |
||||
|
||||
return seconds |
||||
} |
||||
|
||||
override public func didFail(error: Error) { |
||||
Logger.error("failed to send EndSessionMessage with error: \(error.localizedDescription)") |
||||
Storage.writeSync { transaction in |
||||
self.durableOperationDelegate?.durableOperation(self, didFailWithError: error, transaction: transaction) |
||||
|
||||
// Even though this is the failure handler - which means probably the recipient didn't receive the message |
||||
// there's a chance that our send did succeed and the server just timed out our repsonse or something. |
||||
// Since the cost of sending a future message using a session the recipient doesn't have is so high, |
||||
// we archive the session just in case. |
||||
// |
||||
// Archive the just-created session since the recipient should delete their corresponding |
||||
// session upon receiving and decrypting our EndSession message. |
||||
// Otherwise if we send another message before them, they wont have the session to decrypt it. |
||||
self.primaryStorage.archiveAllSessions(forContact: self.recipientId, protocolContext: transaction) |
||||
} |
||||
} |
||||
} |
@ -1,39 +0,0 @@
|
||||
import SessionProtocolKit |
||||
import SessionUtilitiesKit |
||||
|
||||
@objc(SNNullMessage) |
||||
public final class NullMessage : ControlMessage { |
||||
|
||||
// MARK: Initialization |
||||
public override init() { super.init() } |
||||
|
||||
// MARK: Coding |
||||
public required init?(coder: NSCoder) { |
||||
super.init(coder: coder) |
||||
} |
||||
|
||||
public override func encode(with coder: NSCoder) { |
||||
super.encode(with: coder) |
||||
} |
||||
|
||||
// MARK: Proto Conversion |
||||
public override class func fromProto(_ proto: SNProtoContent) -> NullMessage? { |
||||
guard proto.nullMessage != nil else { return nil } |
||||
return NullMessage() |
||||
} |
||||
|
||||
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? { |
||||
let nullMessageProto = SNProtoNullMessage.builder() |
||||
let paddingSize = UInt.random(in: 0..<512) // random(in:) uses the system's default random generator, which is cryptographically secure |
||||
let padding = Data.getSecureRandomData(ofSize: paddingSize)! |
||||
nullMessageProto.setPadding(padding) |
||||
let contentProto = SNProtoContent.builder() |
||||
do { |
||||
contentProto.setNullMessage(try nullMessageProto.build()) |
||||
return try contentProto.build() |
||||
} catch { |
||||
SNLog("Couldn't construct null message proto from: \(self).") |
||||
return nil |
||||
} |
||||
} |
||||
} |
@ -1,80 +0,0 @@
|
||||
import SessionProtocolKit |
||||
import SessionUtilitiesKit |
||||
|
||||
@objc(SNSessionRequest) |
||||
public final class SessionRequest : ControlMessage { |
||||
public var preKeyBundle: PreKeyBundle? |
||||
|
||||
// MARK: Initialization |
||||
public override init() { super.init() } |
||||
|
||||
internal init(preKeyBundle: PreKeyBundle) { |
||||
super.init() |
||||
self.preKeyBundle = preKeyBundle |
||||
} |
||||
|
||||
// MARK: Validation |
||||
public override var isValid: Bool { |
||||
guard super.isValid else { return false } |
||||
return preKeyBundle != nil |
||||
} |
||||
|
||||
// MARK: Coding |
||||
public required init?(coder: NSCoder) { |
||||
super.init(coder: coder) |
||||
if let preKeyBundle = coder.decodeObject(forKey: "preKeyBundle") as! PreKeyBundle? { self.preKeyBundle = preKeyBundle } |
||||
} |
||||
|
||||
public override func encode(with coder: NSCoder) { |
||||
super.encode(with: coder) |
||||
coder.encode(preKeyBundle, forKey: "preKeyBundle") |
||||
} |
||||
|
||||
// MARK: Proto Conversion |
||||
public override class func fromProto(_ proto: SNProtoContent) -> SessionRequest? { |
||||
guard proto.nullMessage != nil, let preKeyBundleProto = proto.prekeyBundleMessage else { return nil } |
||||
var registrationID: UInt32 = 0 |
||||
SNMessagingKitConfiguration.shared.storage.writeSync { transaction in |
||||
registrationID = SNMessagingKitConfiguration.shared.storage.getOrGenerateRegistrationID(using: transaction) |
||||
} |
||||
guard let preKeyBundle = PreKeyBundle( |
||||
registrationId: Int32(registrationID), |
||||
deviceId: 1, |
||||
preKeyId: Int32(preKeyBundleProto.prekeyID), |
||||
preKeyPublic: preKeyBundleProto.prekey, |
||||
signedPreKeyPublic: preKeyBundleProto.signedKey, |
||||
signedPreKeyId: Int32(preKeyBundleProto.signedKeyID), |
||||
signedPreKeySignature: preKeyBundleProto.signature, |
||||
identityKey: preKeyBundleProto.identityKey |
||||
) else { return nil } |
||||
return SessionRequest(preKeyBundle: preKeyBundle) |
||||
} |
||||
|
||||
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? { |
||||
guard let preKeyBundle = preKeyBundle else { |
||||
SNLog("Couldn't construct session request proto from: \(self).") |
||||
return nil |
||||
} |
||||
let nullMessageProto = SNProtoNullMessage.builder() |
||||
let paddingSize = UInt.random(in: 0..<512) // random(in:) uses the system's default random generator, which is cryptographically secure |
||||
let padding = Data.getSecureRandomData(ofSize: paddingSize)! |
||||
nullMessageProto.setPadding(padding) |
||||
let preKeyBundleProto = SNProtoPrekeyBundleMessage.builder() |
||||
preKeyBundleProto.setIdentityKey(preKeyBundle.identityKey) |
||||
preKeyBundleProto.setDeviceID(UInt32(preKeyBundle.deviceId)) |
||||
preKeyBundleProto.setPrekeyID(UInt32(preKeyBundle.preKeyId)) |
||||
preKeyBundleProto.setPrekey(preKeyBundle.preKeyPublic) |
||||
preKeyBundleProto.setSignedKeyID(UInt32(preKeyBundle.signedPreKeyId)) |
||||
preKeyBundleProto.setSignedKey(preKeyBundle.signedPreKeyPublic) |
||||
preKeyBundleProto.setSignature(preKeyBundle.signedPreKeySignature) |
||||
let contentProto = SNProtoContent.builder() |
||||
do { |
||||
contentProto.setNullMessage(try nullMessageProto.build()) |
||||
contentProto.setPrekeyBundleMessage(try preKeyBundleProto.build()) |
||||
return try contentProto.build() |
||||
} catch { |
||||
SNLog("Couldn't construct session request proto from: \(self).") |
||||
return nil |
||||
} |
||||
} |
||||
} |
@ -1,17 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SessionMessagingKit/TSErrorMessage.h> |
||||
|
||||
NS_ASSUME_NONNULL_BEGIN |
||||
|
||||
@interface TSInvalidIdentityKeyErrorMessage : TSErrorMessage |
||||
|
||||
- (void)throws_acceptNewIdentityKey NS_SWIFT_UNAVAILABLE("throws objc exceptions"); |
||||
- (nullable NSData *)throws_newIdentityKey NS_SWIFT_UNAVAILABLE("throws objc exceptions"); |
||||
- (NSString *)theirSignalId; |
||||
|
||||
@end |
||||
|
||||
NS_ASSUME_NONNULL_END |
@ -1,29 +0,0 @@
|
||||
// |
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. |
||||
// |
||||
|
||||
#import "TSInvalidIdentityKeyErrorMessage.h" |
||||
#import <SessionProtocolKit/SessionProtocolKit.h> |
||||
|
||||
NS_ASSUME_NONNULL_BEGIN |
||||
|
||||
@implementation TSInvalidIdentityKeyErrorMessage |
||||
|
||||
- (void)throws_acceptNewIdentityKey |
||||
{ |
||||
|
||||
} |
||||
|
||||
- (nullable NSData *)throws_newIdentityKey |
||||
{ |
||||
return nil; |
||||
} |
||||
|
||||
- (NSString *)theirSignalId |
||||
{ |
||||
return nil; |
||||
} |
||||
|
||||
@end |
||||
|
||||
NS_ASSUME_NONNULL_END |
@ -1,22 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SessionMessagingKit/TSInvalidIdentityKeyErrorMessage.h> |
||||
|
||||
NS_ASSUME_NONNULL_BEGIN |
||||
|
||||
@class SNProtoEnvelope; |
||||
|
||||
// DEPRECATED - we no longer create new instances of this class (as of mid-2017); However, existing instances may
|
||||
// exist, so we should keep this class around to honor their old behavior.
|
||||
__attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage : TSInvalidIdentityKeyErrorMessage |
||||
|
||||
#ifdef DEBUG |
||||
+ (nullable instancetype)untrustedKeyWithEnvelope:(SNProtoEnvelope *)envelope |
||||
withTransaction:(YapDatabaseReadWriteTransaction *)transaction; |
||||
#endif |
||||
|
||||
@end |
||||
|
||||
NS_ASSUME_NONNULL_END |
@ -1,141 +0,0 @@
|
||||
// |
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. |
||||
// |
||||
|
||||
#import "TSInvalidIdentityKeyReceivingErrorMessage.h" |
||||
#import "OWSIdentityManager.h" |
||||
#import "OWSPrimaryStorage.h" |
||||
#import "SSKEnvironment.h" |
||||
#import "TSContactThread.h" |
||||
#import "TSDatabaseView.h" |
||||
#import "TSErrorMessage_privateConstructor.h" |
||||
#import <SessionProtocolKit/NSData+keyVersionByte.h> |
||||
#import <SessionProtocolKit/PreKeyWhisperMessage.h> |
||||
#import <YapDatabase/YapDatabaseTransaction.h> |
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h> |
||||
|
||||
NS_ASSUME_NONNULL_BEGIN |
||||
|
||||
__attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage() |
||||
|
||||
@property (nonatomic, readonly, copy) NSString *authorId; |
||||
|
||||
@end |
||||
|
||||
@implementation TSInvalidIdentityKeyReceivingErrorMessage { |
||||
// Not using a property declaration in order to exclude from DB serialization |
||||
SNProtoEnvelope *_Nullable _envelope; |
||||
} |
||||
|
||||
@synthesize envelopeData = _envelopeData; |
||||
|
||||
#ifdef DEBUG |
||||
// We no longer create these messages, but they might exist on legacy clients so it's useful to be able to |
||||
// create them with the debug UI |
||||
+ (nullable instancetype)untrustedKeyWithEnvelope:(SNProtoEnvelope *)envelope |
||||
withTransaction:(YapDatabaseReadWriteTransaction *)transaction |
||||
{ |
||||
TSContactThread *contactThread = |
||||
[TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; |
||||
|
||||
// Legit usage of senderTimestamp, references message which failed to decrypt |
||||
TSInvalidIdentityKeyReceivingErrorMessage *errorMessage = |
||||
[[self alloc] initForUnknownIdentityKeyWithTimestamp:envelope.timestamp |
||||
inThread:contactThread |
||||
incomingEnvelope:envelope]; |
||||
return errorMessage; |
||||
} |
||||
|
||||
- (nullable instancetype)initForUnknownIdentityKeyWithTimestamp:(uint64_t)timestamp |
||||
inThread:(TSThread *)thread |
||||
incomingEnvelope:(SNProtoEnvelope *)envelope |
||||
{ |
||||
self = [self initWithTimestamp:timestamp inThread:thread failedMessageType:TSErrorMessageWrongTrustedIdentityKey]; |
||||
if (!self) { |
||||
return self; |
||||
} |
||||
|
||||
NSError *error; |
||||
_envelopeData = [envelope serializedDataAndReturnError:&error]; |
||||
if (!_envelopeData || error != nil) { |
||||
return nil; |
||||
} |
||||
|
||||
_authorId = envelope.source; |
||||
|
||||
return self; |
||||
} |
||||
#endif |
||||
|
||||
- (nullable SNProtoEnvelope *)envelope |
||||
{ |
||||
if (!_envelope) { |
||||
NSError *error; |
||||
SNProtoEnvelope *_Nullable envelope = [SNProtoEnvelope parseData:self.envelopeData error:&error]; |
||||
if (error || envelope == nil) { |
||||
|
||||
} else { |
||||
_envelope = envelope; |
||||
} |
||||
} |
||||
return _envelope; |
||||
} |
||||
|
||||
- (void)throws_acceptNewIdentityKey |
||||
{ |
||||
if (self.errorType != TSErrorMessageWrongTrustedIdentityKey) { |
||||
return; |
||||
} |
||||
|
||||
NSData *_Nullable newKey = [self throws_newIdentityKey]; |
||||
if (!newKey) { |
||||
return; |
||||
} |
||||
|
||||
[[OWSIdentityManager sharedManager] saveRemoteIdentity:newKey recipientId:self.envelope.source]; |
||||
|
||||
// Decrypt this and any old messages for the newly accepted key |
||||
NSArray<TSInvalidIdentityKeyReceivingErrorMessage *> *messagesToDecrypt = |
||||
[self.thread receivedMessagesForInvalidKey:newKey]; |
||||
|
||||
for (TSInvalidIdentityKeyReceivingErrorMessage *errorMessage in messagesToDecrypt) { |
||||
|
||||
// Here we remove the existing error message because handleReceivedEnvelope will either |
||||
// 1.) succeed and create a new successful message in the thread or... |
||||
// 2.) fail and create a new identical error message in the thread. |
||||
[errorMessage remove]; |
||||
} |
||||
} |
||||
|
||||
- (nullable NSData *)throws_newIdentityKey |
||||
{ |
||||
if (!self.envelope) { |
||||
return nil; |
||||
} |
||||
|
||||
if (self.envelope.type != SNProtoEnvelopeTypePrekeyBundle) { |
||||
return nil; |
||||
} |
||||
|
||||
NSData *pkwmData = self.envelope.content; |
||||
if (!pkwmData) { |
||||
return nil; |
||||
} |
||||
|
||||
PreKeyWhisperMessage *message = [[PreKeyWhisperMessage alloc] init_throws_withData:pkwmData]; |
||||
return [message.identityKey throws_removeKeyType]; |
||||
} |
||||
|
||||
- (NSString *)theirSignalId |
||||
{ |
||||
if (self.authorId) { |
||||
return self.authorId; |
||||
} else { |
||||
// for existing messages before we were storing author id. |
||||
return self.envelope.source; |
||||
} |
||||
} |
||||
|
||||
@end |
||||
|
||||
NS_ASSUME_NONNULL_END |
@ -1,71 +0,0 @@
|
||||
|
||||
@objc |
||||
public enum UnidentifiedAccessMode: Int { |
||||
case unknown |
||||
case enabled |
||||
case disabled |
||||
case unrestricted |
||||
} |
||||
|
||||
@objc |
||||
public class OWSUDAccess: NSObject { |
||||
@objc |
||||
public let udAccessKey: SMKUDAccessKey |
||||
|
||||
@objc |
||||
public let udAccessMode: UnidentifiedAccessMode |
||||
|
||||
@objc |
||||
public let isRandomKey: Bool |
||||
|
||||
@objc |
||||
public required init(udAccessKey: SMKUDAccessKey, |
||||
udAccessMode: UnidentifiedAccessMode, |
||||
isRandomKey: Bool) { |
||||
self.udAccessKey = udAccessKey |
||||
self.udAccessMode = udAccessMode |
||||
self.isRandomKey = isRandomKey |
||||
} |
||||
} |
||||
|
||||
@objc public protocol OWSUDManager: class { |
||||
|
||||
@objc func setup() |
||||
|
||||
@objc func trustRoot() -> ECPublicKey |
||||
|
||||
@objc func isUDVerboseLoggingEnabled() -> Bool |
||||
|
||||
// MARK: - Recipient State |
||||
|
||||
@objc |
||||
func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, recipientId: String) |
||||
|
||||
@objc |
||||
func unidentifiedAccessMode(forRecipientId recipientId: String) -> UnidentifiedAccessMode |
||||
|
||||
@objc |
||||
func udAccessKey(forRecipientId recipientId: String) -> SMKUDAccessKey? |
||||
|
||||
@objc |
||||
func udAccess(forRecipientId recipientId: String, |
||||
requireSyncAccess: Bool) -> OWSUDAccess? |
||||
|
||||
// MARK: Sender Certificate |
||||
|
||||
// We use completion handlers instead of a promise so that message sending |
||||
// logic can access the strongly typed certificate data. |
||||
@objc |
||||
func ensureSenderCertificate(success:@escaping (SMKSenderCertificate) -> Void, |
||||
failure:@escaping (Error) -> Void) |
||||
|
||||
// MARK: Unrestricted Access |
||||
|
||||
@objc |
||||
func shouldAllowUnrestrictedAccessLocal() -> Bool |
||||
@objc |
||||
func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) |
||||
|
||||
@objc |
||||
func getSenderCertificate() -> SMKSenderCertificate? |
||||
} |