notification: add notification extension
Change-Id: Ic751322ec89f30307178ef6990e2a2509ab1961a
diff --git a/Ring/jamiNotificationExtension/Adapter.mm b/Ring/jamiNotificationExtension/Adapter.mm
new file mode 100644
index 0000000..d6cc919
--- /dev/null
+++ b/Ring/jamiNotificationExtension/Adapter.mm
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2021-2022 Savoir-faire Linux Inc.
+ *
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#import "Adapter.h"
+#import "Utils.h"
+#import "jamiNotificationExtension-Swift.h"
+
+#import "jami/jami.h"
+#import "jami/configurationmanager_interface.h"
+#import "jami/callmanager_interface.h"
+#import "jami/conversation_interface.h"
+#import "jami/datatransfer_interface.h"
+
+#define MSGPACK_DISABLE_LEGACY_NIL
+#import "opendht/crypto.h"
+#import "opendht/default_types.h"
+
+#import "json/json.h"
+#import "fstream"
+#import "charconv"
+
+@implementation Adapter
+
+static id<AdapterDelegate> _delegate;
+
+using namespace DRing;
+
+struct PeerConnectionRequest : public dht::EncryptedValue<PeerConnectionRequest>
+{
+ static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
+ static constexpr const char* key_prefix = "peer:";
+ dht::Value::Id id = dht::Value::INVALID_ID;
+ std::string ice_msg {};
+ bool isAnswer {false};
+ std::string connType {};
+ MSGPACK_DEFINE_MAP(id, ice_msg, isAnswer, connType)
+};
+
+typedef NS_ENUM(NSInteger, NotificationType) { videoCall, audioCall, gitMessage, unknown };
+
+// Constants
+const std::string fileSeparator = "/";
+NSString* const certificates = @"certificates";
+NSString* const crls = @"crls";
+NSString* const ocsp = @"ocsp";
+
+std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers;
+
+#pragma mark Callbacks registration
+- (void)registerSignals
+{
+ confHandlers.insert(exportable_callback<ConfigurationSignal::GetAppDataPath>(
+ [&](const std::string& name, std::vector<std::string>* ret) {
+ if (name == "cache") {
+ auto path = [Constants cachesPath];
+ ret->push_back(std::string([path.path UTF8String]));
+ } else {
+ auto path = [Constants documentsPath];
+ ret->push_back(std::string([path.path UTF8String]));
+ }
+ }));
+
+ confHandlers.insert(exportable_callback<ConversationSignal::MessageReceived>(
+ [&](const std::string& accountId,
+ const std::string& conversationId,
+ std::map<std::string, std::string> message) {
+ if (Adapter.delegate) {
+ NSString* convId = [NSString stringWithUTF8String:conversationId.c_str()];
+ NSString* account = [NSString stringWithUTF8String:accountId.c_str()];
+ NSMutableDictionary* interaction = [Utils mapToDictionnary:message];
+ [Adapter.delegate newInteractionWithConversationId:convId
+ accountId:account
+ message:interaction];
+ }
+ }));
+
+ confHandlers.insert(exportable_callback<DataTransferSignal::DataTransferEvent>(
+ [&](const std::string& account_id,
+ const std::string& conversation_id,
+ const std::string& interaction_id,
+ const std::string& file_id,
+ int eventCode) {
+ if (Adapter.delegate) {
+ NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
+ NSString* conversationId = [NSString stringWithUTF8String:conversation_id.c_str()];
+ NSString* fileId = [NSString stringWithUTF8String:file_id.c_str()];
+ NSString* interactionId = [NSString stringWithUTF8String:interaction_id.c_str()];
+ [Adapter.delegate dataTransferEventWithFileId:fileId
+ withEventCode:eventCode
+ accountId:accountId
+ conversationId:conversationId
+ interactionId:interactionId];
+ }
+ }));
+
+ confHandlers.insert(exportable_callback<ConversationSignal::ConversationSyncFinished>(
+ [&](const std::string& account_id) {
+ if (Adapter.delegate) {
+ NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
+ [Adapter.delegate conversationSyncCompletedWithAccountId:accountId];
+ }
+ }));
+
+ confHandlers.insert(exportable_callback<ConversationSignal::CallConnectionRequest>(
+ [&](const std::string& account_id, const std::string& peer_id, bool hasVideo) {
+ if (Adapter.delegate) {
+ NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
+ NSString* peerId = [NSString stringWithUTF8String:peer_id.c_str()];
+ [Adapter.delegate receivedCallConnectionRequestWithAccountId:accountId
+ peerId:peerId
+ hasVideo:hasVideo];
+ }
+ }));
+ registerSignalHandlers(confHandlers);
+}
+
+#pragma mark AdapterDelegate
++ (id<AdapterDelegate>)delegate
+{
+ return _delegate;
+}
+
++ (void)setDelegate:(id<AdapterDelegate>)delegate
+{
+ _delegate = delegate;
+}
+
+- (bool)downloadFileWithFileId:(NSString*)fileId
+ accountId:(NSString*)accountId
+ conversationId:(NSString*)conversationId
+ interactionId:(NSString*)interactionId
+ withFilePath:(NSString*)filePath
+{
+ return downloadFile(std::string([accountId UTF8String]),
+ std::string([conversationId UTF8String]),
+ std::string([interactionId UTF8String]),
+ std::string([fileId UTF8String]),
+ std::string([filePath UTF8String]));
+}
+
+- (BOOL)start
+{
+ [self registerSignals];
+ if (DRing::initialized() == true) {
+ [self setAccountsActive:true];
+ return true;
+ }
+#if DEBUG
+ int flag = DRing::DRING_FLAG_CONSOLE_LOG | DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_IOS_EXTENSION;
+#else
+ int flag = DRing::DRING_FLAG_IOS_EXTENSION;;
+#endif
+ if (![[NSThread currentThread] isMainThread]) {
+ __block bool success;
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ if (init(static_cast<DRing::InitFlag>(flag))) {
+ success = start({});
+ } else {
+ success = false;
+ }
+ });
+ return success;
+ } else {
+ if (init(static_cast<DRing::InitFlag>(flag))) {
+ return start({});
+ }
+ return false;
+ }
+}
+
+- (void)stop
+{
+ unregisterSignalHandlers();
+ confHandlers.clear();
+ [self setAccountsActive:false];
+}
+
+- (void)setAccountsActive:(BOOL)active
+{
+ auto accounts = getAccountList();
+ for (auto account : accounts) {
+ setAccountActive(account, active);
+ }
+}
+
+- (NSDictionary<NSString*, NSString*>*)decrypt:(NSString*)keyPath
+ treated:(NSString*)treatedMessagesPath
+ value:(NSDictionary*)value
+{
+ if (![[NSFileManager defaultManager] fileExistsAtPath:keyPath]) {
+ return {};
+ }
+
+ NSData* data = [[NSFileManager defaultManager] contentsAtPath:keyPath];
+ const uint8_t* bytes = (const uint8_t*) [data bytes];
+ dht::crypto::PrivateKey dhtKey(bytes, [data length], "");
+
+ Json::Value jsonValue = toJson(value);
+ dht::Value dhtValue(jsonValue);
+
+ if (!dhtValue.isEncrypted()) {
+ return {};
+ }
+ try {
+ dht::Value decrypted = decryptDhtValue(dhtKey, dhtValue);
+ auto unpacked = msgpack::unpack((const char*) decrypted.data.data(), decrypted.data.size());
+ auto peerCR = unpacked.get().as<PeerConnectionRequest>();
+ if (isMessageTreated(peerCR.id, [treatedMessagesPath UTF8String])) {
+ return {};
+ }
+ auto certPath = [[Constants documentsPath] URLByAppendingPathComponent:certificates]
+ .path.UTF8String;
+ auto crlPath = [[Constants documentsPath] URLByAppendingPathComponent:crls].path.UTF8String;
+ auto ocspPath = [[Constants documentsPath] URLByAppendingPathComponent:ocsp].path.UTF8String;
+ std::string peerId = getPeerId(decrypted.owner->getId().toString(),
+ certPath,
+ crlPath,
+ ocspPath);
+ return @{@(peerId.c_str()): @(peerCR.connType.c_str())};
+ } catch (std::runtime_error error) {
+ }
+ return {};
+}
+
+Json::Value
+toJson(NSDictionary* value)
+{
+ Json::Value val;
+ for (NSString* key in value.allKeys) {
+ if ([[value objectForKey:key] isKindOfClass:[NSString class]]) {
+ NSString* stringValue = [value objectForKey:key];
+ val[key.UTF8String] = stringValue.UTF8String;
+ } else if ([[value objectForKey:key] isKindOfClass:[NSNumber class]]) {
+ NSNumber* number = [value objectForKey:key];
+ if ([key isEqualToString:@"id"]) {
+ unsigned long long int intValue = [number unsignedLongLongValue];
+ val[key.UTF8String] = intValue;
+ } else {
+ int intValue = [number intValue];
+ val[key.UTF8String] = intValue;
+ }
+ }
+ }
+ return val;
+}
+
+#pragma mark functions copied from the daemon
+
+#define LIKELY(expr) (expr)
+#define UNLIKELY(expr) (expr)
+
+/*
+ * Check whether a Unicode (5.2) char is in a valid range.
+ *
+ * The first check comes from the Unicode guarantee to never encode
+ * a point above 0x0010ffff, since UTF-16 couldn't represent it.
+ *
+ * The second check covers surrogate pairs (category Cs).
+ *
+ * @param Char the character
+ */
+#define UNICODE_VALID(Char) ((Char) < 0x110000 && (((Char) &0xFFFFF800) != 0xD800))
+
+#define CONTINUATION_CHAR \
+ if ((*(unsigned char*) p & 0xc0) != 0x80) /* 10xxxxxx */ \
+ goto error; \
+ val <<= 6; \
+ val |= (*(unsigned char*) p) & 0x3f;
+
+dht::Value
+decryptDhtValue(const dht::crypto::PrivateKey& key, dht::Value& v)
+{
+ return v.decrypt(key);
+}
+
+template<typename ID = dht::Value::Id>
+bool
+isMessageTreated(ID messageId, const std::string& path)
+{
+ std::ifstream file = std::ifstream(path, std::ios_base::in);
+ if (!file.is_open()) {
+ return false;
+ }
+ std::set<ID, std::less<>> treatedMessages;
+ std::string line;
+ while (std::getline(file, line)) {
+ if constexpr (std::is_same<ID, std::string>::value) {
+ treatedMessages.emplace(std::move(line));
+ } else if constexpr (std::is_integral<ID>::value) {
+ ID vid;
+ if (auto [p, ec] = std::from_chars(line.data(), line.data() + line.size(), vid, 16);
+ ec == std::errc()) {
+ treatedMessages.emplace(vid);
+ }
+ }
+ }
+ return treatedMessages.find(messageId) != treatedMessages.end();
+}
+
+std::string
+getPeerId(const std::string& key,
+ const std::string& certPath,
+ const std::string& crlPath,
+ const std::string& ocspPath)
+{
+ std::map<std::string, std::shared_ptr<dht::crypto::Certificate>> certs;
+ auto dir_content = readDirectory(certPath);
+ unsigned n = 0;
+ for (const auto& f : dir_content) {
+ try {
+ auto crt = std::make_shared<dht::crypto::Certificate>(
+ loadFile(certPath + fileSeparator + f));
+ auto id = crt->getId().toString();
+ auto longId = crt->getLongId().toString();
+ if (id != f && longId != f)
+ throw std::logic_error("Certificate id mismatch");
+ while (crt) {
+ id = crt->getId().toString();
+ longId = crt->getLongId().toString();
+ certs.emplace(std::move(id), crt);
+ certs.emplace(std::move(longId), crt);
+ loadRevocations(*crt, crlPath, ocspPath);
+ crt = crt->issuer;
+ ++n;
+ }
+ } catch (const std::exception& e) {
+ }
+ }
+ auto cit = certs.find(key);
+ if (cit == certs.cend()) {
+ return {};
+ }
+ dht::InfoHash peer_account_id;
+ if (not foundPeerDevice(cit->second, peer_account_id)) {
+ return {};
+ }
+ return peer_account_id.toString();
+}
+
+void
+loadRevocations(dht::crypto::Certificate& crt,
+ const std::string& crlPath,
+ const std::string& ocspPath)
+{
+ auto dir = crlPath + fileSeparator + crt.getId().toString();
+ for (const auto& crl : readDirectory(dir)) {
+ try {
+ crt.addRevocationList(
+ std::make_shared<dht::crypto::RevocationList>(loadFile(dir + fileSeparator + crl)));
+ } catch (const std::exception& e) {
+ }
+ }
+ auto ocsp_dir = ocspPath + fileSeparator + crt.getId().toString();
+ for (const auto& ocsp : readDirectory(ocsp_dir)) {
+ try {
+ std::string ocsp_filepath = ocsp_dir + fileSeparator + ocsp;
+ auto serial = crt.getSerialNumber();
+ if (dht::toHex(serial.data(), serial.size()) != ocsp)
+ continue;
+ dht::Blob ocspBlob = loadFile(ocsp_filepath);
+ crt.ocspResponse = std::make_shared<dht::crypto::OcspResponse>(ocspBlob.data(),
+ ocspBlob.size());
+ } catch (const std::exception& e) {
+ }
+ }
+}
+
+bool
+foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& crt, dht::InfoHash& account_id)
+{
+ if (not crt)
+ return false;
+
+ auto top_issuer = crt;
+ while (top_issuer->issuer)
+ top_issuer = top_issuer->issuer;
+
+ if (top_issuer == crt) {
+ return false;
+ }
+ dht::crypto::TrustList peer_trust;
+ peer_trust.add(*top_issuer);
+ if (not peer_trust.verify(*crt)) {
+ return false;
+ }
+ if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
+ return false;
+ }
+ account_id = crt->issuer->getId();
+ return true;
+}
+
+std::vector<std::string>
+readDirectory(const std::string& dir)
+{
+ NSError* error;
+ NSFileManager* fileMgr = [NSFileManager defaultManager];
+ NSArray* files = [fileMgr contentsOfDirectoryAtPath:@(dir.c_str()) error:&error];
+
+ std::vector<std::string> vector;
+ for (NSString* fileName in files) {
+ vector.push_back([fileName UTF8String]);
+ }
+ return vector;
+}
+
+std::vector<uint8_t>
+loadFile(const std::string& path)
+{
+ if (![[NSFileManager defaultManager] fileExistsAtPath:@(path.c_str())]) {
+ return {};
+ }
+ NSData* data = [[NSFileManager defaultManager] contentsAtPath:@(path.c_str())];
+ return [Utils vectorOfUInt8FromData:data];
+}
+
+std::string
+utf8_make_valid(const std::string& name)
+{
+ ssize_t remaining_bytes = name.size();
+ ssize_t valid_bytes;
+ const char* remainder = name.c_str();
+ const char* invalid;
+ char* str = NULL;
+ char* pos = nullptr;
+
+ while (remaining_bytes != 0) {
+ if (utf8_validate_c_str(remainder, remaining_bytes, &invalid))
+ break;
+
+ valid_bytes = invalid - remainder;
+
+ if (str == NULL)
+ // If every byte is replaced by U+FFFD, max(strlen(string)) == 3 * name.size()
+ str = new char[3 * remaining_bytes];
+
+ pos = str;
+
+ strncpy(pos, remainder, valid_bytes);
+ pos += valid_bytes;
+
+ /* append U+FFFD REPLACEMENT CHARACTER */
+ pos[0] = '\357';
+ pos[1] = '\277';
+ pos[2] = '\275';
+
+ pos += 3;
+
+ remaining_bytes -= valid_bytes + 1;
+ remainder = invalid + 1;
+ }
+
+ if (str == NULL)
+ return std::string(name);
+
+ strncpy(pos, remainder, remaining_bytes);
+ pos += remaining_bytes;
+
+ std::string answer(str, pos - str);
+ assert(utf8_validate_c_str(answer.c_str(), -1, NULL));
+
+ delete[] str;
+
+ return answer;
+}
+
+bool
+utf8_validate_c_str(const char* str, ssize_t max_len, const char** end)
+{
+ const char* p;
+
+ if (max_len < 0)
+ p = fast_validate(str);
+ else
+ p = fast_validate_len(str, max_len);
+
+ if (end)
+ *end = p;
+
+ if ((max_len >= 0 && p != str + max_len) || (max_len < 0 && *p != '\0'))
+ return false;
+ else
+ return true;
+}
+
+static const char*
+fast_validate(const char* str)
+{
+ char32_t val = 0;
+ char32_t min = 0;
+ const char* p;
+
+ for (p = str; *p; p++) {
+ if (*(unsigned char*) p < 128)
+ /* done */;
+ else {
+ const char* last;
+
+ last = p;
+
+ if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
+ if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
+ goto error;
+
+ p++;
+
+ if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
+ goto error;
+ } else {
+ if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
+ min = (1 << 11);
+ val = *(unsigned char*) p & 0x0f;
+ goto TWO_REMAINING;
+ } else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
+ min = (1 << 16);
+ val = *(unsigned char*) p & 0x07;
+ } else
+ goto error;
+
+ p++;
+ CONTINUATION_CHAR;
+ TWO_REMAINING:
+ p++;
+ CONTINUATION_CHAR;
+ p++;
+ CONTINUATION_CHAR;
+
+ if (UNLIKELY(val < min))
+ goto error;
+
+ if (UNLIKELY(!UNICODE_VALID(val)))
+ goto error;
+ }
+
+ continue;
+
+ error:
+ return last;
+ }
+ }
+
+ return p;
+}
+
+static const char*
+fast_validate_len(const char* str, ssize_t max_len)
+{
+ char32_t val = 0;
+ char32_t min = 0;
+ const char* p;
+
+ assert(max_len >= 0);
+
+ for (p = str; ((p - str) < max_len) && *p; p++) {
+ if (*(unsigned char*) p < 128)
+ /* done */;
+ else {
+ const char* last;
+
+ last = p;
+
+ if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
+ if (UNLIKELY(max_len - (p - str) < 2))
+ goto error;
+
+ if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
+ goto error;
+
+ p++;
+
+ if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
+ goto error;
+ } else {
+ if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
+ if (UNLIKELY(max_len - (p - str) < 3))
+ goto error;
+
+ min = (1 << 11);
+ val = *(unsigned char*) p & 0x0f;
+ goto TWO_REMAINING;
+ } else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
+ if (UNLIKELY(max_len - (p - str) < 4))
+ goto error;
+
+ min = (1 << 16);
+ val = *(unsigned char*) p & 0x07;
+ } else
+ goto error;
+
+ p++;
+ CONTINUATION_CHAR;
+ TWO_REMAINING:
+ p++;
+ CONTINUATION_CHAR;
+ p++;
+ CONTINUATION_CHAR;
+
+ if (UNLIKELY(val < min))
+ goto error;
+
+ if (UNLIKELY(!UNICODE_VALID(val)))
+ goto error;
+ }
+
+ continue;
+
+ error:
+ return last;
+ }
+ }
+
+ return p;
+}
+
+@end