Created
August 25, 2017 04:26
-
-
Save laeroah/aeabc9bfd7e96a8ab5e6b20d979e575d to your computer and use it in GitHub Desktop.
A sample `AFEncryptionDelegate` implementation. You can refer to this gist when you need to create a encryption extension for AppFriends.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// EncryptionManager.swift | |
// HacknocraftBaseProject | |
// | |
// Created by HAO WANG on 7/31/17. | |
// Copyright © 2017 Hacknocraft. All rights reserved. | |
// | |
import Foundation | |
import VirgilSDK | |
import RxSwift | |
import AppFriendsCore | |
import AppFriendsUI | |
struct VirgilConstants { | |
static let ChatUser = "SecureChatUser" | |
static let PrivateKeyStorage = "AppFriendsPrivateKeyStorage" | |
} | |
class EncryptionManager: NSObject { | |
let virgil: VSSVirgilApi? | |
let disposeBag = DisposeBag() | |
var virgilCards: [String: VSSVirgilCard] = [String: VSSVirgilCard]() | |
static let sharedInstance = EncryptionManager() | |
override init() { | |
// initialize Virgil | |
if let virgilToken = EnvConfig.envConfigValue(forKey: "virgilToken") { | |
virgil = VSSVirgilApi(token: virgilToken) | |
} else { | |
virgil = nil | |
} | |
super.init() | |
} | |
// MARK: - Virgil Cards | |
func obtainCardForCurrentUser(identity: String, password: String? = nil) -> Observable<Void> { | |
// try find and create a virgil card for the current user | |
return searchForExistingCardCurrentUser(identity: identity, password: password) | |
.flatMap({ (card) -> Observable<Void> in | |
if let existingCard = card { | |
do { | |
_ = try self.virgil?.keys.loadKey(withName: identity, password: password) | |
return self.publishAndVerifyCard(userID: identity, card: existingCard) | |
} catch { | |
return Observable.create({ (observer) -> Disposable in | |
observer.onError(error) | |
return Disposables.create() | |
}) | |
} | |
} else { | |
return self.createCardForCurrentUser(identity: identity, password: password) | |
.flatMap({ (card) in | |
self.publishAndVerifyCard(userID: identity, card: card) | |
}) | |
} | |
}) | |
} | |
func createCardForCurrentUser(identity: String, password: String? = nil) -> Observable<VSSVirgilCard> { | |
return Observable.create({ (observer) -> Disposable in | |
guard let virgil = self.virgil else { | |
// virgil not initialized | |
let errorReason = NSLocalizedString("virgil not initialized", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
observer.onError(err) | |
return Disposables.create() | |
} | |
var currentUserKey: VSSVirgilKey? | |
do { | |
// try find the key first from keychain | |
currentUserKey = try virgil.keys | |
.loadKey(withName: identity, | |
password: password) | |
} catch { } | |
if currentUserKey == nil { | |
// key not found, create a new one | |
currentUserKey = virgil.keys.generateKey() | |
do { | |
try currentUserKey?.store(withName: identity, password: password) | |
} catch { | |
observer.onError(error) | |
return Disposables.create() | |
} | |
} | |
if let userKey = currentUserKey { | |
do { | |
let identityInfo = virgil.identities | |
.createUserIdentity(withValue: identity, | |
type: VirgilConstants.ChatUser) | |
// create a Virgil Card | |
let card = try virgil.cards | |
.createCard(with: identityInfo, ownerKey:userKey) | |
observer.onNext(card) | |
} catch { | |
observer.onError(error) | |
} | |
} else { | |
observer.onCompleted() | |
} | |
return Disposables.create() | |
}) | |
} | |
func searchForExistingCardCurrentUser(identity: String, | |
password: String? = nil) -> Observable<VSSVirgilCard?> { | |
return Observable.create({ (observer) -> Disposable in | |
if let card = self.virgilCards[identity] { | |
observer.onNext(card) | |
return Disposables.create() | |
} | |
guard let virgil = self.virgil else { | |
// virgil not initialized | |
let errorReason = NSLocalizedString("virgil not initialized", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
observer.onError(err) | |
return Disposables.create() | |
} | |
virgil.cards.searchCards(withIdentities: [identity]) { cards, error in | |
if let candidates = cards, candidates.count > 0 { | |
self.virgilCards[identity] = candidates[0] | |
observer.onNext(candidates[0]) | |
} else if let err = error { | |
observer.onError(err) | |
} else { | |
observer.onNext(nil) | |
} | |
} | |
return Disposables.create() | |
}) | |
} | |
func searchForCards(identities: [String], | |
password: String? = nil) -> Observable<[VSSVirgilCard]> { | |
return Observable.create({ (observer) -> Disposable in | |
var cards = [VSSVirgilCard]() | |
for identity in identities { | |
if let card = self.virgilCards[identity] { | |
cards.append(card) | |
} | |
} | |
if cards.count == identities.count { | |
// all the cards were found locally, no need to search | |
observer.onNext(cards) | |
return Disposables.create() | |
} | |
guard let virgil = self.virgil else { | |
// virgil not initialized | |
let errorReason = NSLocalizedString("virgil not initialized", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
observer.onError(err) | |
return Disposables.create() | |
} | |
virgil.cards.searchCards(withIdentities: identities) { cards, error in | |
if let candidates = cards, candidates.count > 0 { | |
for card in candidates { | |
self.virgilCards[card.identity] = card | |
} | |
observer.onNext(candidates) | |
} else if let err = error { | |
observer.onError(err) | |
} else { | |
let errorReason = NSLocalizedString("virgil cards not found", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
observer.onError(err) | |
} | |
} | |
return Disposables.create() | |
}) | |
} | |
func publishAndVerifyCard(userID id: String, card: VSSVirgilCard) -> Observable<Void> { | |
return Observable.create({ (observer) -> Disposable in | |
let path = "/users/\(id)/virgil_card" | |
HCSDKCore.sharedInstance.startRequest(httpMethod: "POST", | |
path: path, | |
parameters: ["content": card.exportData() as NSString], | |
completion: { (_, error) in | |
if let err = error { | |
observer.onError(err) | |
} else { | |
observer.onNext() | |
} | |
}) | |
return Disposables.create() | |
}) | |
} | |
// MARK: - Encrypt and Decrypt | |
func encrypt(texts: [String], | |
forUsers userIDs: [String], | |
completion: ((_ encryptedTexts: [String]?, _ error: Error?) -> Void)?) { | |
guard let virgil = self.virgil else { | |
// virgil not initialized | |
let errorReason = NSLocalizedString("virgil not initialized", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
completion?(nil, err) | |
return | |
} | |
guard let currentUserID = AFSession.currentUserID() else { | |
let errorReason = NSLocalizedString("user not logged in", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .notLoggedIn) | |
completion?(nil, err) | |
return | |
} | |
do { | |
// try find the key first from keychain | |
let currentUserKey = try virgil.keys.loadKey(withName: currentUserID, | |
password: nil) | |
// always encrypt with the current user's card | |
var usersToSearch = [String]() | |
usersToSearch.append(contentsOf: userIDs) | |
if !usersToSearch.contains(currentUserID) { | |
usersToSearch.append(currentUserID) | |
} | |
searchForCards(identities: usersToSearch) | |
.subscribe(onNext: { (cards) in | |
// encrypt the buffer using found Virgil Cards | |
do { | |
var encryptedTexts = [String]() | |
for text in texts { | |
let ciphertext = try currentUserKey.signThenEncrypt(text, for: cards) | |
.base64EncodedString() | |
encryptedTexts.append(ciphertext) | |
} | |
completion?(encryptedTexts, nil) | |
} catch { | |
let errorReason = NSLocalizedString("virgil encryption failed", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .encryptionError) | |
completion?(nil, err) | |
} | |
}, onError: { (error) in | |
log.error(error.localizedDescription) | |
completion?(nil, error) | |
}).addDisposableTo(disposeBag) | |
} catch { | |
let errorReason = NSLocalizedString("missing private key", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
completion?(nil, err) | |
return | |
} | |
} | |
func decrypt(encryptedTexts: [String], | |
forUsers userIDs: [String], | |
completion: ((_ encryptedTexts: [String]?, _ error: Error?) -> Void)?) { | |
guard let virgil = self.virgil else { | |
// virgil not initialized | |
let errorReason = NSLocalizedString("virgil not initialized", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
completion?(nil, err) | |
return | |
} | |
guard let currentUserID = AFSession.currentUserID() else { | |
let errorReason = NSLocalizedString("user not logged in", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .notLoggedIn) | |
completion?(nil, err) | |
return | |
} | |
do { | |
// try find the key first from keychain | |
let currentUserKey = try virgil.keys.loadKey(withName: currentUserID, | |
password: nil) | |
searchForCards(identities: userIDs) | |
.subscribe(onNext: { (cards) in | |
// encrypt the buffer using found Virgil Cards | |
do { | |
var decryptedTexts = [String]() | |
for encrypted in encryptedTexts { | |
let decryptedData = try currentUserKey.decryptThenVerify(base64: encrypted, | |
fromOneOf: cards) | |
if let decryptedText = String(data: decryptedData, encoding: .utf8) { | |
decryptedTexts.append(decryptedText) | |
} | |
} | |
completion?(decryptedTexts, nil) | |
} catch { | |
completion?(nil, error) | |
} | |
}, onError: { (error) in | |
completion?(nil, error) | |
}).addDisposableTo(disposeBag) | |
} catch { | |
let errorReason = NSLocalizedString("missing private key", | |
comment: "") | |
let err = HCError.createError(withDescription: errorReason, | |
domain: .appfriends, | |
code: .badParameters) | |
completion?(nil, err) | |
return | |
} | |
} | |
} | |
// MARK: - AFEncryptionDelegate | |
extension EncryptionManager: AFEncryptionDelegate { | |
func encrypt(texts: [String], forUser userID: String, completion: (([String]?, Error?) -> Void)?) { | |
encrypt(texts: texts, forUsers: [userID], completion: completion) | |
} | |
func decrypt(encryptedTexts: [String], forUser userID: String, completion: (([String]?, Error?) -> Void)?) { | |
decrypt(encryptedTexts: encryptedTexts, forUsers: [userID], completion: completion) | |
} | |
func encrypt(texts: [String], forDialog dialogID: String, completion: (([String]?, Error?) -> Void)?) { | |
AFDialog.getDialog(dialogID: dialogID) { (dialog, error) in | |
if let err = error { | |
completion?(nil, err) | |
} else if let chatDialog = dialog { | |
self.encrypt(texts: texts, | |
forUsers: chatDialog.memberIDs(), | |
completion: completion) | |
} else { | |
completion?(nil, nil) | |
} | |
} | |
} | |
func decrypt(encryptedTexts: [String], forDialog dialogID: String, completion: (([String]?, Error?) -> Void)?) { | |
AFDialog.getDialog(dialogID: dialogID) { (dialog, error) in | |
if let err = error { | |
completion?(nil, err) | |
} else if let chatDialog = dialog { | |
self.decrypt(encryptedTexts: encryptedTexts, | |
forUsers: chatDialog.memberIDs(), | |
completion: completion) | |
} else { | |
completion?(nil, nil) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment