Explorar o código

feat(sync): impl age-encryption

Andelf %!s(int64=3) %!d(string=hai) anos
pai
achega
28a08a0252

+ 4 - 0
ios/App/App.xcodeproj/project.pbxproj

@@ -34,6 +34,7 @@
 		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
 		FE8C946B27FD762700C8017B /* FileSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946927FD762700C8017B /* FileSync.swift */; };
 		FE8C946C27FD762700C8017B /* FileSync.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946A27FD762700C8017B /* FileSync.m */; };
+		FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE688A328448F8C0019510E /* AgeEncryption.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -97,6 +98,7 @@
 		FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
 		FE8C946927FD762700C8017B /* FileSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSync.swift; sourceTree = "<group>"; };
 		FE8C946A27FD762700C8017B /* FileSync.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileSync.m; sourceTree = "<group>"; };
+		FEE688A328448F8C0019510E /* AgeEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeEncryption.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -205,6 +207,7 @@
 				FE443F1D27FF54AA007ECE65 /* Payload.swift */,
 				FE443F1B27FF5420007ECE65 /* Extensions.swift */,
 				FE8C946A27FD762700C8017B /* FileSync.m */,
+				FEE688A328448F8C0019510E /* AgeEncryption.swift */,
 			);
 			path = FileSync;
 			sourceTree = "<group>";
@@ -362,6 +365,7 @@
 				5FD5BB71278579F5008E6875 /* DownloadiCloudFiles.swift in Sources */,
 				FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */,
 				5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
+				FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */,
 				FE8C946B27FD762700C8017B /* FileSync.swift in Sources */,
 				FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
 				5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,

+ 95 - 0
ios/App/App/FileSync/AgeEncryption.swift

@@ -0,0 +1,95 @@
+//
+//  AgeEncryption.swift
+//  Logseq
+//
+//  Created by Mono Wang on 5/30/R4.
+//
+
+import Foundation
+import AgeEncryption
+
+public enum AgeEncryption {
+    public static func keygen() -> (String, String) {
+        let cSecretKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
+        let cPublicKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
+        
+        rust_age_encryption_keygen(cSecretKey, cPublicKey);
+        let secretKey = String(cString: cSecretKey.pointee!)
+        let publicKey = String(cString: cPublicKey.pointee!)
+        
+        rust_age_encryption_free_str(cSecretKey.pointee!)
+        rust_age_encryption_free_str(cPublicKey.pointee!)
+        cSecretKey.deallocate()
+        cPublicKey.deallocate()
+
+        return (secretKey, publicKey)
+    }
+    
+    public static func encryptWithPassphrase(_ plaintext: Data, _ passphrase: String, armor: Bool) -> Data? {
+        plaintext.withUnsafeBytes { (cPlaintext) in
+            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
+
+            let ret = rust_age_encrypt_with_user_passphrase(passphrase.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
+            if ret > 0 {
+                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
+                let ciphertext = Data.init(buffer: cOutputBuf)
+                rust_age_encryption_free_vec(cOutput.pointee, ret)
+                cOutput.deallocate()
+                return ciphertext
+            } else {
+                return nil
+            }
+        }
+    }
+
+    public static func decryptWithPassphrase(_ ciphertext: Data, _ passphrase: String) -> Data? {
+        ciphertext.withUnsafeBytes { (cCiphertext) in
+            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
+
+            let ret = rust_age_decrypt_with_user_passphrase(passphrase.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
+            if ret > 0 {
+                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
+                let plaintext = Data.init(buffer: cOutputBuf)
+                rust_age_encryption_free_vec(cOutput.pointee, ret)
+                cOutput.deallocate()
+                return plaintext
+            } else {
+                return nil
+            }
+        }
+    }
+    
+    public static func encryptWithX25519(_ plaintext: Data, _ publicKey: String, armor: Bool) -> Data? {
+        plaintext.withUnsafeBytes { (cPlaintext) in
+            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
+            
+            let ret = rust_age_encrypt_with_x25519(publicKey.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
+            if ret > 0 {
+                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
+                let ciphertext = Data.init(buffer: cOutputBuf)
+                rust_age_encryption_free_vec(cOutput.pointee, ret)
+                cOutput.deallocate()
+                return ciphertext
+            } else {
+                return nil
+            }
+        }
+    }
+    
+    public static func decryptWithX25519(_ ciphertext: Data, _ secretKey: String) -> Data? {
+        ciphertext.withUnsafeBytes { (cCiphertext) in
+            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
+
+            let ret = rust_age_decrypt_with_x25519(secretKey.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
+            if ret >= 0 {
+                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
+                let plaintext = Data.init(buffer: cOutputBuf)
+                rust_age_encryption_free_vec(cOutput.pointee, ret)
+                cOutput.deallocate()
+                return plaintext
+            } else {
+                return nil
+            }
+        }
+    }
+}

+ 45 - 0
ios/App/App/FileSync/Data+ChaChaPoly.swift

@@ -0,0 +1,45 @@
+//
+//  Data+ChaChaPoly.swift
+//  Logseq
+//
+//  Created by Mono Wang on 5/20/R4.
+//
+
+import Foundation
+import CryptoKit
+
+extension Data {
+    /**
+     Encrypts current data using ChaChaPoly cipher.
+     */
+    public func sealChaChaPoly(with passphrase: String) -> Data? {
+        guard let symmetricKey = try? SymmetricKey(passwordString: passphrase) else {
+            return nil
+        }
+
+        let nonce = try? ChaChaPoly.Nonce(data: Data(repeating: 0x00, count: 12))
+        if let encrypted = try? ChaChaPoly.seal(self, using: symmetricKey, nonce: nonce!).combined {
+            var ret = Data(hexEncoded: "4c530031")
+            ret?.append(encrypted)
+            return ret
+        }
+        return nil
+    }
+
+    /**
+     Decrypts current combined ChaChaPoly Selead box data (nonce || ciphertext || tag) using ChaChaPoly cipher.
+     */
+    public func openChaChaPoly(with passphrase: String) -> Data? {
+        if self.count <= 4 {
+            return nil
+        }
+        guard let symmetricKey = try? SymmetricKey(passwordString: passphrase) else {
+            return nil
+        }
+        guard let chaChaPolySealedBox = try? ChaChaPoly.SealedBox(combined: self[4...]) else {
+            return nil
+        }
+
+        return try? ChaChaPoly.open(chaChaPolySealedBox, using: symmetricKey)
+    }
+}

+ 28 - 59
ios/App/App/FileSync/Extensions.swift

@@ -50,25 +50,6 @@ extension Array where Element == UInt8 {
   }
 }
 
-@available(iOS 13.0, *)
-extension SymmetricKey {
-    public init(passwordString keyString: String) throws {
-        let size = SymmetricKeySize.bits256
-        guard var keyData = keyString.data(using: .utf8) else {
-            print("Could not create raw Data from String.")
-            throw CryptoKitError.incorrectParameterSize
-        }
-            
-        let keySizeBytes = size.bitCount / 8
-        keyData = keyData.subdata(in: 0..<keySizeBytes)
-        guard keyData.count >= keySizeBytes else { throw CryptoKitError.incorrectKeySize }
-        
-        print("debug key \(keyData) \(keyData.hexDescription)")
-        
-        self.init(data: keyData)
-    }
-}
-
 extension Data {
     public init?(hexEncoded: String) {
         self.init(Array<UInt8>(hex: hexEncoded))
@@ -78,38 +59,9 @@ extension Data {
         return map { String(format: "%02hhx", $0) }.joined()
     }
     
-    @available(iOS 13.0, *)
-    func aesEncrypt(keyString: String) throws -> Data {
-        let key = try? SymmetricKey(passwordString: keyString)
-        
-        let nonce = Data(hexEncoded: "131348c0987c7eece60fc0bc") // = initialization vector
-        let tag = Data(hexEncoded: "5baa85ff3e7eda3204744ec74b71d523")
-        
-        print("debug tag \(tag?.hexDescription) nonce \(nonce?.hexDescription)")
-        let sealedData = try! AES.GCM.seal(self, using: key!, nonce: AES.GCM.Nonce(data: nonce!), authenticating: tag!)
-            
-        print("debug encrypted \(sealedData)")
-        guard let encryptedContent = sealedData.combined else {
-            throw CryptoKitError.underlyingCoreCryptoError(error: 2)
-        }
-        print("debug encrypted \(encryptedContent)")
-        print("debug encrypted \(encryptedContent.hexDescription)")
-        print("debug tag \(sealedData.tag.hexDescription)")
-        return encryptedContent
-    }
-    
-    @available(iOS 13.0, *)
-    func aesDecrypt(keyString: String) throws -> Data {
-        let key = try! SymmetricKey(passwordString: keyString)
-        let tag = Data(hexEncoded: "5baa85ff3e7eda3204744ec74b71d523")
-        
-        guard let sealedBox = try? AES.GCM.SealedBox(combined: self) else {
-            throw CryptoKitError.authenticationFailure
-        }
-        guard let decryptedData = try? AES.GCM.open(sealedBox, using: key, authenticating: tag!) else {
-            throw CryptoKitError.authenticationFailure
-        }
-        return decryptedData
+    var MD5: String {
+        let computed = Insecure.MD5.hash(data: self)
+        return computed.map { String(format: "%02hhx", $0) }.joined()
     }
 }
 
@@ -159,8 +111,8 @@ extension URL {
         return relComponents.joined(separator: "/")
     }
     
+    // Download a remote URL to a file
     func download(toFile file: URL, completion: @escaping (Error?) -> Void) {
-        // Download the remote URL to a file
         let task = URLSession.shared.downloadTask(with: self) {
             (tempURL, response, error) in
             // Early exit on error
@@ -188,14 +140,16 @@ extension URL {
                 // Remove any existing document at file
                 if FileManager.default.fileExists(atPath: file.path) {
                     try FileManager.default.removeItem(at: file)
+                } else {
+                    let baseURL = file.deletingLastPathComponent()
+                    try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
+                }
+                let rawData = try Data(contentsOf: tempURL)
+                guard let decryptedRawData = maybeDecrypt(rawData) else {
+                    throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "can not decrypt remote file"])
                 }
-                
-                // Copy the tempURL to file
-                try FileManager.default.copyItem(
-                    at: tempURL,
-                    to: file
-                )
-                
+                try decryptedRawData.write(to: file, options: .atomic)
+
                 completion(nil)
             }
             
@@ -209,3 +163,18 @@ extension URL {
         task.resume()
     }
 }
+
+// MARK: Crypto helper
+
+extension SymmetricKey {
+    public init(passwordString keyString: String) throws {
+        guard let keyData = keyString.data(using: .utf8) else {
+            print("ERROR: Could not create raw Data from String")
+            throw CryptoKitError.incorrectParameterSize
+        }
+        // SymmetricKeySize.bits256
+        let keyDigest = SHA256.hash(data: keyData)
+        
+        self.init(data: keyDigest)
+    }
+}

+ 93 - 35
ios/App/App/FileSync/FileSync.swift

@@ -10,13 +10,18 @@ import Foundation
 import AWSMobileClient
 import CryptoKit
 
-// MARK: Global variables
+
+// MARK: Global variable
 
 // Defualts to dev
 var URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
 var BUCKET: String = "logseq-file-sync-bucket"
 var REGION: String = "us-east-2"
 
+var ENCRYPTION_SECRET_KEY: String? = nil
+var ENCRYPTION_PUBLIC_KEY: String? = nil
+
+// MARK: Metadata type
 
 public struct SyncMetadata: CustomStringConvertible, Equatable {
     var md5: String
@@ -29,9 +34,9 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
                 return nil
             }
             size = fileAttributes.fileSize ?? 0
-            
-            // incremental MD5sum
-            let bufferSize = 1024 * 1024
+
+            // incremental MD5 checksum
+            let bufferSize = 512 * 1024
             let file = try FileHandle(forReadingFrom: fileURL)
             defer {
                 file.closeFile()
@@ -46,7 +51,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
                     return false // eof
                 }
             }) {}
-            
+
             let computed = ctx.finalize()
             md5 = computed.map { String(format: "%02hhx", $0) }.joined()
         } catch {
@@ -59,6 +64,36 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
     }
 }
 
+// MARK: encryption helper
+
+func maybeEncrypt(_ plaindata: Data) -> Data! {
+    // avoid encryption twice
+    if plaindata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
+        plaindata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
+        return plaindata
+    }
+    if let publicKey = ENCRYPTION_PUBLIC_KEY {
+        // use armor = false, for smaller size
+        if let cipherdata = AgeEncryption.encryptWithPassphrase(plaindata, publicKey, armor: false) {
+            return cipherdata
+        }
+        return nil // encryption fail
+    }
+    return plaindata
+}
+
+func maybeDecrypt(_ cipherdata: Data) -> Data! {
+    if cipherdata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) {
+        return cipherdata
+    }
+    if let secretKey = ENCRYPTION_SECRET_KEY {
+        if let plaindata = AgeEncryption.decryptWithX25519(cipherdata, secretKey) {
+            return plaindata
+        }
+        return nil
+    }
+    return cipherdata
+}
 
 // MARK: FileSync Plugin
 
@@ -66,6 +101,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
 public class FileSync: CAPPlugin, SyncDebugDelegate {
     override public func load() {
         print("debug File sync iOS plugin loaded!")
+
         AWSMobileClient.default().initialize { (userState, error) in
             guard error == nil else {
                 print("error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
@@ -73,17 +109,40 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             }
         }
     }
-    
+
     // NOTE: for debug, or an activity indicator
     public func debugNotification(_ message: [String: Any]) {
         self.notifyListeners("debug", data: message)
     }
-    
+
+    @objc func keygen(_ call: CAPPluginCall) {
+        let (secretKey, publicKey) = AgeEncryption.keygen()
+        call.resolve(["secretKey": secretKey,
+                      "publicKey": publicKey])
+    }
+
+    @objc func setKey(_ call: CAPPluginCall) {
+        let secretKey = call.getString("secretKey")
+        let publicKey = call.getString("publicKey")
+        if secretKey == nil && publicKey == nil {
+            ENCRYPTION_SECRET_KEY = nil
+            ENCRYPTION_PUBLIC_KEY = nil
+        }
+        guard let secretKey = secretKey, let publicKey = publicKey else {
+            call.reject("both secretKey and publicKey should be provided")
+            return
+        }
+        ENCRYPTION_SECRET_KEY = secretKey
+        ENCRYPTION_PUBLIC_KEY = publicKey
+    }
+
     @objc func setEnv(_ call: CAPPluginCall) {
         guard let env = call.getString("env") else {
             call.reject("required parameter: env")
             return
         }
+        self.setKey(call)
+
         switch env {
         case "production", "product", "prod":
             URL_BASE = URL(string: "https://api-prod.logseq.com/file-sync/")!
@@ -97,10 +156,11 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("invalid env: \(env)")
             return
         }
+
         self.debugNotification(["event": "setenv:\(env)"])
         call.resolve(["ok": true])
     }
-    
+
     @objc func getLocalFilesMeta(_ call: CAPPluginCall) {
         guard let basePath = call.getString("basePath"),
               let filePaths = call.getArray("filePaths") as? [String] else {
@@ -111,7 +171,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("invalid basePath")
             return
         }
-        
+
         var fileMetadataDict: [String: [String: Any]] = [:]
         for filePath in filePaths {
             let url = baseURL.appendingPathComponent(filePath)
@@ -120,20 +180,20 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                                               "size": meta.size]
             }
         }
-        
+
         call.resolve(["result": fileMetadataDict])
     }
-    
+
     @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
         guard let basePath = call.getString("basePath"),
               let baseURL = URL(string: basePath) else {
                   call.reject("invalid basePath")
                   return
               }
-        
+
         var fileMetadataDict: [String: [String: Any]] = [:]
         if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) {
-            
+
             for case let fileURL as URL in enumerator {
                 if !fileURL.isSkipped() {
                     if let meta = SyncMetadata(of: fileURL) {
@@ -147,8 +207,8 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         }
         call.resolve(["result": fileMetadataDict])
     }
-    
-    
+
+
     @objc func renameLocalFile(_ call: CAPPluginCall) {
         guard let basePath = call.getString("basePath"),
               let baseURL = URL(string: basePath) else {
@@ -163,10 +223,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("invalid to file")
             return
         }
-        
+
         let fromUrl = baseURL.appendingPathComponent(from)
         let toUrl = baseURL.appendingPathComponent(to)
-        
+
         do {
             try FileManager.default.moveItem(at: fromUrl, to: toUrl)
         } catch {
@@ -174,23 +234,23 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             return
         }
         call.resolve(["ok": true])
-        
+
     }
-    
+
     @objc func deleteLocalFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
               let filePaths = call.getArray("filePaths") as? [String] else {
                   call.reject("required paremeters: basePath, filePaths")
                   return
               }
-        
+
         for filePath in filePaths {
             let fileUrl = baseURL.appendingPathComponent(filePath)
             try? FileManager.default.removeItem(at: fileUrl) // ignore any delete errors
         }
         call.resolve(["ok": true])
     }
-    
+
     /// remote -> local
     @objc func updateLocalFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
@@ -200,10 +260,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                   call.reject("required paremeters: basePath, filePaths, graphUUID, token")
                   return
               }
-        
+
         let client = SyncClient(token: token, graphUUID: graphUUID)
         client.delegate = self // receives notification
-        
+
         client.getFiles(at: filePaths) {  (fileURLs, error) in
             if let error = error {
                 print("debug getFiles error \(error)")
@@ -212,9 +272,9 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             } else {
                 // handle multiple completionHandlers
                 let group = DispatchGroup()
-                
+
                 var downloaded: [String] = []
-                
+
                 for (filePath, remoteFileURL) in fileURLs {
                     group.enter()
 
@@ -235,11 +295,11 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                     self.debugNotification(["event": "download:done"])
                     call.resolve(["ok": true, "data": downloaded])
                 }
-                
+
             }
         }
     }
-    
+
     @objc func deleteRemoteFiles(_ call: CAPPluginCall) {
         guard let filePaths = call.getArray("filePaths") as? [String],
               let graphUUID = call.getString("graphUUID"),
@@ -252,7 +312,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("empty filePaths")
             return
         }
-        
+
         let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
         client.deleteFiles(filePaths) { txid, error in
             guard error == nil else {
@@ -266,7 +326,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.resolve(["ok": true, "txid": txid])
         }
     }
-    
+
     /// local -> remote
     @objc func updateRemoteFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
@@ -280,12 +340,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         guard !filePaths.isEmpty else {
             return call.reject("empty filePaths")
         }
-        
-        print("debug begin updateRemoteFiles \(filePaths)")
-        
+
         let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
         client.delegate = self
-        
+
         // 1. refresh_temp_credential
         client.getTempCredential() { (credentials, error) in
             guard error == nil else {
@@ -293,14 +351,14 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                 call.reject("error(getTempCredential): \(error!)")
                 return
             }
-            
+
             var files: [String: URL] = [:]
             for filePath in filePaths {
                 // NOTE: filePath from js may contain spaces
                 let fileURL = baseURL.appendingPathComponent(filePath)
                 files[filePath.encodeAsFname()] = fileURL
             }
-            
+
             // 2. upload_temp_file
             client.uploadTempFiles(files, credentials: credentials!) { (uploadedFileKeyDict, error) in
                 guard error == nil else {

+ 2 - 1
ios/App/App/FileSync/SyncClient.swift

@@ -303,13 +303,14 @@ public class SyncClient {
         for (filePath, fileLocalURL) in files {
             print("debug, upload temp \(fileLocalURL) \(filePath)")
             guard let rawData = try? Data(contentsOf: fileLocalURL) else { continue }
+            guard let encryptedRawDat = maybeEncrypt(rawData) else { continue }
             group.enter()
             
             let randFileName = String.random(length: 15).appending(".").appending(fileLocalURL.pathExtension)
             let key = "\(self.s3prefix!)/ios\(randFileName)"
 
             keyFileDict[key] = filePath
-            transferUtility?.uploadData(rawData, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
+            transferUtility?.uploadData(encryptedRawDat, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
                 .continueWith(block: { (task) in
                     if let error = task.error {
                         completionHandler([:], error)

+ 23 - 0
ios/App/LogseqSpecs/AgeEncryption.podspec

@@ -0,0 +1,23 @@
+Pod::Spec.new do |s|
+    s.name             = "AgeEncryption"
+    s.version          = "1.0.3"
+    s.summary          = "AgeEncryption for Logseq"
+    s.description      = <<-DESC
+                         TODO: Add description
+                         DESC
+    s.homepage         = "https://github.com/andelf/AgeEncryption"
+    s.license          = 'MIT'
+    s.author           = { "Andelf" => "[email protected]" }
+    s.source           = { :http => "https://github.com/andelf/AgeEncryption/releases/download/1.0.3/AgeEncryption.xcframework.zip" }
+    #s.source = { }
+
+    s.requires_arc          = true
+
+    s.platform = :ios
+    s.ios.deployment_target = '12.0'
+
+    s.vendored_frameworks = "AgeEncryption.xcframework"
+    s.static_framework = true
+
+    s.swift_version = '5.1'
+  end

+ 1 - 0
ios/App/Podfile

@@ -27,4 +27,5 @@ target 'Logseq' do
   # Add your Pods here
   pod 'AWSMobileClient'
   pod 'AWSS3'
+  pod 'AgeEncryption', :podspec => './LogseqSpecs/AgeEncryption.podspec'
 end

+ 2 - 1
src/electron/electron/file_sync_rsapi.cljs

@@ -1,7 +1,8 @@
 (ns electron.file-sync-rsapi
   (:require ["@logseq/rsapi" :as rsapi]))
 
-(defn set-env [env passphrase] (rsapi/setEnv env passphrase))
+(defn set-env [env secret-key public-key]
+  (rsapi/setEnv env secret-key public-key))
 
 (defn get-local-files-meta [graph-uuid base-path file-paths]
   (rsapi/getLocalFilesMeta graph-uuid base-path (clj->js file-paths)))

+ 18 - 7
src/main/frontend/fs/sync.cljs

@@ -409,7 +409,7 @@
 ;; `RSAPI` call apis through rsapi package, supports operations on files
 
 (defprotocol IRSAPI
-  (set-env [this prod? passphrase] "set environment")
+  (set-env [this prod? secret-key public-key] "set environment and passphrase")
   (get-local-files-meta [this graph-uuid base-path filepaths] "get local files' metadata")
   (get-local-all-files-meta [this graph-uuid base-path] "get all local files' metadata")
   (rename-local-file [this graph-uuid base-path from to])
@@ -460,7 +460,10 @@
       (<! (user/refresh-id-token&access-token))
       (state/get-auth-id-token)))
   IRSAPI
-  (set-env [_ prod? passphrase] (go (<! (p->c (ipc/ipc "set-env" (if prod? "prod" "dev") passphrase)))))
+  (set-env [_ prod? secret-key public-key]
+    (when (not-empty secret-key)
+      (print "[sync] setting sync age-encryption passphrase..."))
+    (go (<! (p->c (ipc/ipc "set-env" (if prod? "prod" "dev") secret-key public-key)))))
   (get-local-all-files-meta [_ graph-uuid base-path]
     (go
       (let [r (<! (retry-rsapi #(p->c (ipc/ipc "get-local-all-files-meta" graph-uuid base-path))))]
@@ -526,8 +529,10 @@
       (state/get-auth-id-token)))
 
   IRSAPI
-  (set-env [_ prod? _passphrase]      ;; TODO passphrase
-    (go (<! (p->c (.setEnv mobile-util/file-sync (clj->js {:env (if prod? "prod" "dev")}))))))
+  (set-env [_ prod? secret-key public-key]
+    (go (<! (p->c (.setEnv mobile-util/file-sync (clj->js {:env (if prod? "prod" "dev")
+                                                           :secretKey secret-key
+                                                           :publicKey public-key}))))))
 
   (get-local-all-files-meta [_ _graph-uuid base-path]
     (go
@@ -1266,7 +1271,7 @@
                             (map #(relative-path %))
                             (filter #(not (contains-path? ignore-files %))))
               paths (sequence es->paths-xf es)]
-          (println "sync-local->remote" paths)
+          (println "sync-local->remote" type paths)
           (let [r (case type
                     ("add" "change")
                     (update-remote-files rsapi graph-uuid base-path paths @*txid)
@@ -1413,7 +1418,11 @@
     (need-password [this]
       (go
         (<! (ensure-pwd-exists! (state/get-current-repo) graph-uuid))
-        (<! (set-env rsapi config/FILE-SYNC-PROD? (get @pwd-map graph-uuid)))
+        (<! (set-env rsapi config/FILE-SYNC-PROD?
+                     "AGE-SECRET-KEY-1RRP2D43M00FTPARY5MJNN0Z4D6K8NDWC9ME5P60ZE59EDKMXP9PQK0P6YA"
+                     "age1sk2zx4lxcy47tjcgmfdz65sxcpw92k8fjpdencmcgyncxtexfupsz38tcg"
+                     ;; (get @pwd-map graph-uuid)
+                     ))
         (.schedule this ::idle nil)))
 
     (idle [this]
@@ -1595,7 +1604,9 @@
              (clear-graphs-txid! repo)
              (do
                ;; set-env
-               (<! (set-env rsapi config/FILE-SYNC-PROD? ""))
+               (<! (set-env rsapi config/FILE-SYNC-PROD?
+                            "AGE-SECRET-KEY-1RRP2D43M00FTPARY5MJNN0Z4D6K8NDWC9ME5P60ZE59EDKMXP9PQK0P6YA"
+                            "age1sk2zx4lxcy47tjcgmfdz65sxcpw92k8fjpdencmcgyncxtexfupsz38tcg"))
                (state/set-file-sync-state repo @*sync-state)
                (state/set-file-sync-manager sm)
                ;; wait seconds to receive all file change events,

+ 4 - 0
src/main/frontend/handler/file_sync.cljs

@@ -99,6 +99,10 @@
   (let [repo (state/get-current-repo)
         base-path (config/get-repo-dir repo)
         user-uuid (user/user-uuid)]
+    ;; FIXME: when switching graph, sync-start is not called. set-env is not called as well.
+    (sync/set-env sync/rsapi config/FILE-SYNC-PROD?
+                  "AGE-SECRET-KEY-1RRP2D43M00FTPARY5MJNN0Z4D6K8NDWC9ME5P60ZE59EDKMXP9PQK0P6YA"
+                  "age1sk2zx4lxcy47tjcgmfdz65sxcpw92k8fjpdencmcgyncxtexfupsz38tcg")
     (sync/update-graphs-txid! 0 graph-uuid user-uuid repo)
     (go (sync/sync-stop)
         (<! (download-all-files repo graph-uuid user-uuid base-path))