From 0e7a19c6b7b329e707fdedef50f4c8b264b62788 Mon Sep 17 00:00:00 2001
From: Yiqun Zhang <guiyanakuang@gmail.com>
Date: Tue, 21 Nov 2023 15:32:33 +0800
Subject: [PATCH] :sparkles: Create a tooltip for clipevery to interact with
 keychain

---
 .../clipevery/macos/MacosClipboardService.kt  |   4 +-
 .../clipevery/macos/MacosKeychainHelper.kt    |  22 +++++
 .../clipevery/macos/api/MacClipboardApi.kt    |  13 ---
 .../com/clipevery/macos/api/MacosApi.kt       |  21 ++++
 .../resources/darwin-x86-64/libMacosApi.dylib | Bin 0 -> 58936 bytes
 .../src/desktopMain/swift/MacosApi.swift      |  93 ++++++++++++++++++
 .../macos/MacosKeychainHelperTest.kt          |  24 +++++
 7 files changed, 162 insertions(+), 15 deletions(-)
 create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosKeychainHelper.kt
 delete mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt
 create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacosApi.kt
 create mode 100755 composeApp/src/desktopMain/resources/darwin-x86-64/libMacosApi.dylib
 create mode 100644 composeApp/src/desktopMain/swift/MacosApi.swift
 create mode 100644 composeApp/src/desktopTest/kotlin/com/clipevery/macos/MacosKeychainHelperTest.kt

diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboardService.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboardService.kt
index 133cdafd5..2e05c8369 100644
--- a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboardService.kt
+++ b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboardService.kt
@@ -1,7 +1,7 @@
 package com.clipevery.macos
 
 import com.clipevery.clip.ClipboardService
-import com.clipevery.macos.api.MacClipboard
+import com.clipevery.macos.api.MacosApi
 import java.awt.Toolkit
 import java.awt.datatransfer.Clipboard
 import java.awt.datatransfer.Transferable
@@ -22,7 +22,7 @@ class MacosClipboardService
 
     override fun run() {
         try {
-            MacClipboard.INSTANCE.clipboardChangeCount.let { currentChangeCount ->
+            MacosApi.INSTANCE.getClipboardChangeCount().let { currentChangeCount ->
                 if (changeCount == currentChangeCount) {
                     return
                 }
diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosKeychainHelper.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosKeychainHelper.kt
new file mode 100644
index 000000000..1d7f389cd
--- /dev/null
+++ b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosKeychainHelper.kt
@@ -0,0 +1,22 @@
+package com.clipevery.macos
+
+import com.clipevery.macos.api.MacosApi
+
+object MacosKeychainHelper {
+
+    fun getPassword(service: String, account: String): String? {
+        return MacosApi.INSTANCE.getPassword(service, account)
+    }
+
+    fun setPassword(service: String, account: String, password: String): Boolean {
+        return MacosApi.INSTANCE.setPassword(service, account, password)
+    }
+
+    fun updatePassword(service: String, account: String, password: String): Boolean {
+        return MacosApi.INSTANCE.updatePassword(service, account, password)
+    }
+
+    fun deletePassword(service: String, account: String): Boolean {
+        return MacosApi.INSTANCE.deletePassword(service, account)
+    }
+}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt
deleted file mode 100644
index f194412a8..000000000
--- a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.clipevery.macos.api
-
-import com.sun.jna.Library
-import com.sun.jna.Native
-
-
-interface MacClipboard : Library {
-    val clipboardChangeCount: Int
-
-    companion object {
-        val INSTANCE: MacClipboard = Native.load("ClipboardHelper", MacClipboard::class.java)
-    }
-}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacosApi.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacosApi.kt
new file mode 100644
index 000000000..adbdbeb78
--- /dev/null
+++ b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacosApi.kt
@@ -0,0 +1,21 @@
+package com.clipevery.macos.api
+
+import com.sun.jna.Library
+import com.sun.jna.Native
+
+interface MacosApi : Library {
+
+    fun getClipboardChangeCount(): Int
+
+    fun getPassword(service: String, account: String): String?
+
+    fun setPassword(service: String, account: String, password: String): Boolean
+
+    fun updatePassword(service: String, account: String, password: String): Boolean
+
+    fun deletePassword(service: String, account: String): Boolean
+
+    companion object {
+        val INSTANCE: MacosApi = Native.load("MacosApi", MacosApi::class.java)
+    }
+}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/resources/darwin-x86-64/libMacosApi.dylib b/composeApp/src/desktopMain/resources/darwin-x86-64/libMacosApi.dylib
new file mode 100755
index 0000000000000000000000000000000000000000..9ab0371f243faa65a2bcabdc8053c37b462eff63
GIT binary patch
literal 58936
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
Jr)ZeK^RE<CsxANk

literal 0
HcmV?d00001

diff --git a/composeApp/src/desktopMain/swift/MacosApi.swift b/composeApp/src/desktopMain/swift/MacosApi.swift
new file mode 100644
index 000000000..4c0c1665e
--- /dev/null
+++ b/composeApp/src/desktopMain/swift/MacosApi.swift
@@ -0,0 +1,93 @@
+import Cocoa
+import Foundation
+import Security
+
+@_cdecl("getClipboardChangeCount")
+public func getClipboardChangeCount() -> Int {
+    let pasteboard = NSPasteboard.general
+    return pasteboard.changeCount
+}
+
+@_cdecl("getPassword")
+public func getPassword(service: UnsafePointer<CChar>, account: UnsafePointer<CChar>) -> UnsafePointer<CChar>? {
+    let serviceString = String(cString: service)
+    let accountString = String(cString: account)
+
+    let query: [String: Any] = [
+        kSecClass as String: kSecClassGenericPassword,
+        kSecAttrService as String: serviceString,
+        kSecAttrAccount as String: accountString,
+        kSecReturnData as String: kCFBooleanTrue!,
+        kSecMatchLimit as String: kSecMatchLimitOne
+    ]
+
+    var item: CFTypeRef?
+    let status = SecItemCopyMatching(query as CFDictionary, &item)
+
+    guard status == errSecSuccess else { return nil }
+
+    if let data = item as? Data, let password = String(data: data, encoding: .utf8) {
+        return UnsafePointer<CChar>(strdup(password))
+    }
+
+    return nil
+}
+
+@_cdecl("setPassword")
+public func setPassword(service: UnsafePointer<CChar>, account: UnsafePointer<CChar>, password: UnsafePointer<CChar>) -> Bool {
+    let serviceString = String(cString: service)
+    let accountString = String(cString: account)
+    let passwordString = String(cString: password)
+
+    let passwordData = passwordString.data(using: .utf8)!
+
+    let query: [String: Any] = [
+        kSecClass as String: kSecClassGenericPassword,
+        kSecAttrService as String: serviceString,
+        kSecAttrAccount as String: accountString,
+        kSecValueData as String: passwordData
+    ]
+
+    // Try to add the item to the keychain
+    let status = SecItemAdd(query as CFDictionary, nil)
+
+    // Check the result
+    return status == errSecSuccess
+}
+
+@_cdecl("updatePassword")
+public func updatePassword(service: UnsafePointer<CChar>, account: UnsafePointer<CChar>, newPassword: UnsafePointer<CChar>) -> Bool {
+    let serviceString = String(cString: service)
+    let accountString = String(cString: account)
+    let newPasswordString = String(cString: newPassword)
+
+    let query: [String: Any] = [
+        kSecClass as String: kSecClassGenericPassword,
+        kSecAttrService as String: serviceString,
+        kSecAttrAccount as String: accountString
+    ]
+
+    let updateFields: [String: Any] = [
+        kSecValueData as String: newPasswordString.data(using: .utf8)!
+    ]
+
+    let status = SecItemUpdate(query as CFDictionary, updateFields as CFDictionary)
+
+    return status == errSecSuccess
+}
+
+@_cdecl("deletePassword")
+public func deletePassword(service: UnsafePointer<CChar>, account: UnsafePointer<CChar>) -> Bool {
+    let serviceString = String(cString: service)
+    let accountString = String(cString: account)
+
+    let query: [String: Any] = [
+        kSecClass as String: kSecClassGenericPassword,
+        kSecAttrService as String: serviceString,
+        kSecAttrAccount as String: accountString
+    ]
+
+    let status = SecItemDelete(query as CFDictionary)
+
+    return status == errSecSuccess
+}
\ No newline at end of file
diff --git a/composeApp/src/desktopTest/kotlin/com/clipevery/macos/MacosKeychainHelperTest.kt b/composeApp/src/desktopTest/kotlin/com/clipevery/macos/MacosKeychainHelperTest.kt
new file mode 100644
index 000000000..259520de0
--- /dev/null
+++ b/composeApp/src/desktopTest/kotlin/com/clipevery/macos/MacosKeychainHelperTest.kt
@@ -0,0 +1,24 @@
+package com.clipevery.macos
+
+import com.clipevery.platform.currentPlatform
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class MacosKeychainHelperTest {
+
+    @Test
+    fun testKeychain() {
+        if (currentPlatform().isMacos()) {
+            MacosKeychainHelper.setPassword("com.clipevery", "test", "test")
+            var password = MacosKeychainHelper.getPassword("com.clipevery", "test")
+            assertEquals("test", password)
+            MacosKeychainHelper.updatePassword("com.clipevery", "test", "test1")
+            password = MacosKeychainHelper.getPassword("com.clipevery", "test")
+            assertEquals("test1", password)
+            assertTrue(MacosKeychainHelper.deletePassword("com.clipevery", "test"))
+            val password1 = MacosKeychainHelper.getPassword("com.clipevery", "test")
+            assertTrue(password1 == null)
+        }
+    }
+}
\ No newline at end of file