Extensions.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. //
  2. // Extensions.swift
  3. // Logseq
  4. //
  5. // Created by Mono Wang on 4/8/R4.
  6. //
  7. import Foundation
  8. import CryptoKit
  9. import var CommonCrypto.CC_MD5_DIGEST_LENGTH
  10. import func CommonCrypto.CC_MD5
  11. import typealias CommonCrypto.CC_LONG
  12. // via https://github.com/krzyzanowskim/CryptoSwift
  13. extension Array where Element == UInt8 {
  14. public init(hex: String) {
  15. self = Array.init()
  16. self.reserveCapacity(hex.unicodeScalars.lazy.underestimatedCount)
  17. var buffer: UInt8?
  18. var skip = hex.hasPrefix("0x") ? 2 : 0
  19. for char in hex.unicodeScalars.lazy {
  20. guard skip == 0 else {
  21. skip -= 1
  22. continue
  23. }
  24. guard char.value >= 48 && char.value <= 102 else {
  25. removeAll()
  26. return
  27. }
  28. let v: UInt8
  29. let c: UInt8 = UInt8(char.value)
  30. switch c {
  31. case let c where c <= 57:
  32. v = c - 48
  33. case let c where c >= 65 && c <= 70:
  34. v = c - 55
  35. case let c where c >= 97:
  36. v = c - 87
  37. default:
  38. removeAll()
  39. return
  40. }
  41. if let b = buffer {
  42. append(b << 4 | v)
  43. buffer = nil
  44. } else {
  45. buffer = v
  46. }
  47. }
  48. if let b = buffer {
  49. append(b)
  50. }
  51. }
  52. }
  53. @available(iOS 13.0, *)
  54. extension SymmetricKey {
  55. public init(passwordString keyString: String) throws {
  56. let size = SymmetricKeySize.bits256
  57. guard var keyData = keyString.data(using: .utf8) else {
  58. print("Could not create raw Data from String.")
  59. throw CryptoKitError.incorrectParameterSize
  60. }
  61. let keySizeBytes = size.bitCount / 8
  62. keyData = keyData.subdata(in: 0..<keySizeBytes)
  63. guard keyData.count >= keySizeBytes else { throw CryptoKitError.incorrectKeySize }
  64. print("debug key \(keyData) \(keyData.hexDescription)")
  65. self.init(data: keyData)
  66. }
  67. }
  68. extension Data {
  69. public init?(hexEncoded: String) {
  70. self.init(Array<UInt8>(hex: hexEncoded))
  71. }
  72. var hexDescription: String {
  73. return map { String(format: "%02hhx", $0) }.joined()
  74. }
  75. @available(iOS 13.0, *)
  76. func aesEncrypt(keyString: String) throws -> Data {
  77. let key = try? SymmetricKey(passwordString: keyString)
  78. let nonce = Data(hexEncoded: "131348c0987c7eece60fc0bc") // = initialization vector
  79. let tag = Data(hexEncoded: "5baa85ff3e7eda3204744ec74b71d523")
  80. print("debug tag \(tag?.hexDescription) nonce \(nonce?.hexDescription)")
  81. let sealedData = try! AES.GCM.seal(self, using: key!, nonce: AES.GCM.Nonce(data: nonce!), authenticating: tag!)
  82. print("debug encrypted \(sealedData)")
  83. guard let encryptedContent = sealedData.combined else {
  84. throw CryptoKitError.underlyingCoreCryptoError(error: 2)
  85. }
  86. print("debug encrypted \(encryptedContent)")
  87. print("debug encrypted \(encryptedContent.hexDescription)")
  88. print("debug tag \(sealedData.tag.hexDescription)")
  89. return encryptedContent
  90. }
  91. @available(iOS 13.0, *)
  92. func aesDecrypt(keyString: String) throws -> Data {
  93. let key = try! SymmetricKey(passwordString: keyString)
  94. let tag = Data(hexEncoded: "5baa85ff3e7eda3204744ec74b71d523")
  95. guard let sealedBox = try? AES.GCM.SealedBox(combined: self) else {
  96. throw CryptoKitError.authenticationFailure
  97. }
  98. guard let decryptedData = try? AES.GCM.open(sealedBox, using: key, authenticating: tag!) else {
  99. throw CryptoKitError.authenticationFailure
  100. }
  101. return decryptedData
  102. }
  103. }
  104. extension String {
  105. var MD5: String {
  106. // TODO: incremental hash
  107. if #available(iOS 13.0, *) {
  108. let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
  109. return computed.map { String(format: "%02hhx", $0) }.joined()
  110. } else {
  111. // Fallback on earlier versions, no CryptoKit
  112. let length = Int(CC_MD5_DIGEST_LENGTH)
  113. let messageData = self.data(using:.utf8)!
  114. var digestData = Data(count: length)
  115. _ = digestData.withUnsafeMutableBytes { digestBytes -> UInt8 in
  116. messageData.withUnsafeBytes { messageBytes -> UInt8 in
  117. if let messageBytesBaseAddress = messageBytes.baseAddress, let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress {
  118. let messageLength = CC_LONG(messageData.count)
  119. CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory)
  120. }
  121. return 0
  122. }
  123. }
  124. return digestData.map { String(format: "%02hhx", $0) }.joined()
  125. }
  126. }
  127. func encodeAsFname() -> String {
  128. var allowed = NSMutableCharacterSet.urlPathAllowed
  129. allowed.remove(charactersIn: "&$@=;:+ ,?%#")
  130. return self.addingPercentEncoding(withAllowedCharacters: allowed) ?? self
  131. }
  132. func decodeFromFname() -> String {
  133. return self.removingPercentEncoding ?? self
  134. }
  135. static func random(length: Int) -> String {
  136. let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  137. return String((0..<length).map{ _ in letters.randomElement()! })
  138. }
  139. }
  140. extension URL {
  141. func relativePath(from base: URL) -> String? {
  142. // Ensure that both URLs represent files:
  143. guard self.isFileURL && base.isFileURL else {
  144. return nil
  145. }
  146. // Remove/replace "." and "..", make paths absolute:
  147. let destComponents = self.standardized.pathComponents
  148. let baseComponents = base.standardized.pathComponents
  149. // Find number of common path components:
  150. var i = 0
  151. while i < destComponents.count && i < baseComponents.count
  152. && destComponents[i] == baseComponents[i] {
  153. i += 1
  154. }
  155. // Build relative path:
  156. var relComponents = Array(repeating: "..", count: baseComponents.count - i)
  157. relComponents.append(contentsOf: destComponents[i...])
  158. return relComponents.joined(separator: "/")
  159. }
  160. func download(toFile file: URL, completion: @escaping (Error?) -> Void) {
  161. // Download the remote URL to a file
  162. let task = URLSession.shared.downloadTask(with: self) {
  163. (tempURL, response, error) in
  164. // Early exit on error
  165. guard let tempURL = tempURL else {
  166. completion(error)
  167. return
  168. }
  169. if let response = response! as? HTTPURLResponse {
  170. if response.statusCode == 404 {
  171. completion(NSError(domain: "",
  172. code: response.statusCode,
  173. userInfo: [NSLocalizedDescriptionKey: "remote file not found"]))
  174. return
  175. }
  176. if response.statusCode != 200 {
  177. completion(NSError(domain: "",
  178. code: response.statusCode,
  179. userInfo: [NSLocalizedDescriptionKey: "invalid http status code"]))
  180. return
  181. }
  182. }
  183. do {
  184. // Remove any existing document at file
  185. if FileManager.default.fileExists(atPath: file.path) {
  186. try FileManager.default.removeItem(at: file)
  187. }
  188. // Copy the tempURL to file
  189. try FileManager.default.copyItem(
  190. at: tempURL,
  191. to: file
  192. )
  193. completion(nil)
  194. }
  195. // Handle potential file system errors
  196. catch {
  197. completion(error)
  198. }
  199. }
  200. // Start the download
  201. task.resume()
  202. }
  203. }