ios: local video encryption (#3682)
* ios: local video encryption * main thread * new progress view * simplify * rename --------- Co-authored-by: Avently <avently@local> Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
d5cf9fbf5b
commit
88640b85c4
@ -158,7 +158,8 @@ func imageHasAlpha(_ img: UIImage) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
|
func saveFileFromURL(_ url: URL) -> CryptoFile? {
|
||||||
|
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
|
||||||
let savedFile: CryptoFile?
|
let savedFile: CryptoFile?
|
||||||
if url.startAccessingSecurityScopedResource() {
|
if url.startAccessingSecurityScopedResource() {
|
||||||
do {
|
do {
|
||||||
@ -185,10 +186,19 @@ func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
|
|||||||
|
|
||||||
func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
|
func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
|
||||||
do {
|
do {
|
||||||
|
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
|
||||||
let fileName = uniqueCombine(url.lastPathComponent)
|
let fileName = uniqueCombine(url.lastPathComponent)
|
||||||
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
|
let savedFile: CryptoFile?
|
||||||
|
if encrypted {
|
||||||
|
let cfArgs = try encryptCryptoFile(fromPath: url.path, toPath: getAppFilePath(fileName).path)
|
||||||
|
try FileManager.default.removeItem(atPath: url.path)
|
||||||
|
savedFile = CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
|
||||||
|
} else {
|
||||||
|
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
|
||||||
|
savedFile = CryptoFile.plain(fileName)
|
||||||
|
}
|
||||||
ChatModel.shared.filesToDelete.remove(url)
|
ChatModel.shared.filesToDelete.remove(url)
|
||||||
return CryptoFile.plain(fileName)
|
return savedFile
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)")
|
logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)")
|
||||||
return nil
|
return nil
|
||||||
|
@ -855,8 +855,8 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
|||||||
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
|
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiveFile(user: any UserLike, fileId: Int64, encrypted: Bool, auto: Bool = false) async {
|
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
||||||
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: encrypted, auto: auto) {
|
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
||||||
await chatItemSimpleUpdate(user, chatItem)
|
await chatItemSimpleUpdate(user, chatItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1516,7 +1516,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
}
|
}
|
||||||
if let file = cItem.autoReceiveFile() {
|
if let file = cItem.autoReceiveFile() {
|
||||||
Task {
|
Task {
|
||||||
await receiveFile(user: user, fileId: file.fileId, encrypted: cItem.encryptLocalFile, auto: true)
|
await receiveFile(user: user, fileId: file.fileId, auto: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cItem.showNotification {
|
if cItem.showNotification {
|
||||||
|
@ -85,8 +85,7 @@ struct CIFileView: View {
|
|||||||
Task {
|
Task {
|
||||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
||||||
if let user = m.currentUser {
|
if let user = m.currentUser {
|
||||||
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
|
await receiveFile(user: user, fileId: file.fileId)
|
||||||
await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,7 +38,7 @@ struct CIImageView: View {
|
|||||||
case .rcvInvitation:
|
case .rcvInvitation:
|
||||||
Task {
|
Task {
|
||||||
if let user = m.currentUser {
|
if let user = m.currentUser {
|
||||||
await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile)
|
await receiveFile(user: user, fileId: file.fileId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .rcvAccepted:
|
case .rcvAccepted:
|
||||||
|
@ -26,6 +26,8 @@ struct CIVideoView: View {
|
|||||||
@State private var player: AVPlayer?
|
@State private var player: AVPlayer?
|
||||||
@State private var fullPlayer: AVPlayer?
|
@State private var fullPlayer: AVPlayer?
|
||||||
@State private var url: URL?
|
@State private var url: URL?
|
||||||
|
@State private var urlDecrypted: URL?
|
||||||
|
@State private var decryptionInProgress: Bool = false
|
||||||
@State private var showFullScreenPlayer = false
|
@State private var showFullScreenPlayer = false
|
||||||
@State private var timeObserver: Any? = nil
|
@State private var timeObserver: Any? = nil
|
||||||
@State private var fullScreenTimeObserver: Any? = nil
|
@State private var fullScreenTimeObserver: Any? = nil
|
||||||
@ -39,8 +41,12 @@ struct CIVideoView: View {
|
|||||||
self._videoWidth = videoWidth
|
self._videoWidth = videoWidth
|
||||||
self.scrollProxy = scrollProxy
|
self.scrollProxy = scrollProxy
|
||||||
if let url = getLoadedVideo(chatItem.file) {
|
if let url = getLoadedVideo(chatItem.file) {
|
||||||
self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(url, false))
|
let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
|
||||||
self._fullPlayer = State(initialValue: AVPlayer(url: url))
|
self._urlDecrypted = State(initialValue: decrypted)
|
||||||
|
if let decrypted = decrypted {
|
||||||
|
self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(decrypted, false))
|
||||||
|
self._fullPlayer = State(initialValue: AVPlayer(url: decrypted))
|
||||||
|
}
|
||||||
self._url = State(initialValue: url)
|
self._url = State(initialValue: url)
|
||||||
}
|
}
|
||||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||||
@ -53,8 +59,10 @@ struct CIVideoView: View {
|
|||||||
let file = chatItem.file
|
let file = chatItem.file
|
||||||
ZStack {
|
ZStack {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
if let file = file, let preview = preview, let player = player, let url = url {
|
if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted {
|
||||||
videoView(player, url, file, preview, duration)
|
videoView(player, decrypted, file, preview, duration)
|
||||||
|
} else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil {
|
||||||
|
videoViewEncrypted(file, defaultPreview, duration)
|
||||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||||
let uiImage = UIImage(data: data) {
|
let uiImage = UIImage(data: data) {
|
||||||
imageView(uiImage)
|
imageView(uiImage)
|
||||||
@ -62,7 +70,7 @@ struct CIVideoView: View {
|
|||||||
if let file = file {
|
if let file = file {
|
||||||
switch file.fileStatus {
|
switch file.fileStatus {
|
||||||
case .rcvInvitation:
|
case .rcvInvitation:
|
||||||
receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
|
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||||
case .rcvAccepted:
|
case .rcvAccepted:
|
||||||
switch file.fileProtocol {
|
switch file.fileProtocol {
|
||||||
case .xftp:
|
case .xftp:
|
||||||
@ -88,7 +96,7 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
if let file = file, case .rcvInvitation = file.fileStatus {
|
if let file = file, case .rcvInvitation = file.fileStatus {
|
||||||
Button {
|
Button {
|
||||||
receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
|
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||||
} label: {
|
} label: {
|
||||||
playPauseIcon("play.fill")
|
playPauseIcon("play.fill")
|
||||||
}
|
}
|
||||||
@ -96,6 +104,40 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
|
||||||
|
return ZStack(alignment: .topTrailing) {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete
|
||||||
|
imageView(defaultPreview)
|
||||||
|
.fullScreenCover(isPresented: $showFullScreenPlayer) {
|
||||||
|
if let decrypted = urlDecrypted {
|
||||||
|
fullScreenPlayer(decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
decrypt(file: file) {
|
||||||
|
showFullScreenPlayer = urlDecrypted != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !decryptionInProgress {
|
||||||
|
Button {
|
||||||
|
decrypt(file: file) {
|
||||||
|
if let decrypted = urlDecrypted {
|
||||||
|
videoPlaying = true
|
||||||
|
player?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||||
|
}
|
||||||
|
.disabled(!canBePlayed)
|
||||||
|
} else {
|
||||||
|
videoDecryptionProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
|
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
|
||||||
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
|
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
|
||||||
DispatchQueue.main.async { videoWidth = w }
|
DispatchQueue.main.async { videoWidth = w }
|
||||||
@ -159,6 +201,16 @@ struct CIVideoView: View {
|
|||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func videoDecryptionProgress(_ color: Color = .white) -> some View {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
.tint(color)
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.background(Color.black.opacity(0.35))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
private func durationProgress() -> some View {
|
private func durationProgress() -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("\(durationText(videoPlaying ? progress : duration))")
|
Text("\(durationText(videoPlaying ? progress : duration))")
|
||||||
@ -257,10 +309,10 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO encrypt: where file size is checked?
|
// TODO encrypt: where file size is checked?
|
||||||
private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
|
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
|
||||||
Task {
|
Task {
|
||||||
if let user = m.currentUser {
|
if let user = m.currentUser {
|
||||||
await receiveFile(user, file.fileId, encrypted, false)
|
await receiveFile(user, file.fileId, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,6 +375,22 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func decrypt(file: CIFile, completed: (() -> Void)? = nil) {
|
||||||
|
if decryptionInProgress { return }
|
||||||
|
decryptionInProgress = true
|
||||||
|
Task {
|
||||||
|
urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete)
|
||||||
|
await MainActor.run {
|
||||||
|
if let decrypted = urlDecrypted {
|
||||||
|
player = VideoPlayerView.getOrCreatePlayer(decrypted, false)
|
||||||
|
fullPlayer = AVPlayer(url: decrypted)
|
||||||
|
}
|
||||||
|
decryptionInProgress = true
|
||||||
|
completed?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func addObserver(_ player: AVPlayer, _ url: URL) {
|
private func addObserver(_ player: AVPlayer, _ url: URL) {
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in
|
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in
|
||||||
if let item = player.currentItem {
|
if let item = player.currentItem {
|
||||||
|
@ -221,7 +221,7 @@ struct VoiceMessagePlayer: View {
|
|||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
if let user = chatModel.currentUser {
|
if let user = chatModel.currentUser {
|
||||||
await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get())
|
await receiveFile(user: user, fileId: recordingFile.fileId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -689,7 +689,7 @@ struct ComposeView: View {
|
|||||||
let file = voiceCryptoFile(recordingFileName)
|
let file = voiceCryptoFile(recordingFileName)
|
||||||
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl)
|
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl)
|
||||||
case let .filePreview(_, file):
|
case let .filePreview(_, file):
|
||||||
if let savedFile = saveFileFromURL(file, encrypted: privacyEncryptLocalFilesGroupDefault.get()) {
|
if let savedFile = saveFileFromURL(file) {
|
||||||
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -631,7 +631,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||||||
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
||||||
}
|
}
|
||||||
if let file = cItem.autoReceiveFile() {
|
if let file = cItem.autoReceiveFile() {
|
||||||
cItem = autoReceiveFile(file, encrypted: cItem.encryptLocalFile) ?? cItem
|
cItem = autoReceiveFile(file) ?? cItem
|
||||||
}
|
}
|
||||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
||||||
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
|
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
|
||||||
@ -775,7 +775,8 @@ func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
|
|||||||
logger.error("setFileToReceive error: \(responseError(r))")
|
logger.error("setFileToReceive error: \(responseError(r))")
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoReceiveFile(_ file: CIFile, encrypted: Bool) -> ChatItem? {
|
func autoReceiveFile(_ file: CIFile) -> ChatItem? {
|
||||||
|
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
|
||||||
switch file.fileProtocol {
|
switch file.fileProtocol {
|
||||||
case .smp:
|
case .smp:
|
||||||
return apiReceiveFile(fileId: file.fileId, encrypted: encrypted)?.chatItem
|
return apiReceiveFile(fileId: file.fileId, encrypted: encrypted)?.chatItem
|
||||||
|
@ -2242,11 +2242,6 @@ public struct ChatItem: Identifiable, Decodable {
|
|||||||
return fileSource.cryptoArgs != nil
|
return fileSource.cryptoArgs != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public var encryptLocalFile: Bool {
|
|
||||||
content.msgContent?.isVideo == false &&
|
|
||||||
privacyEncryptLocalFilesGroupDefault.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
public var memberDisplayName: String? {
|
public var memberDisplayName: String? {
|
||||||
get {
|
get {
|
||||||
if case let .groupRcv(groupMember) = chatDir {
|
if case let .groupRcv(groupMember) = chatDir {
|
||||||
@ -2910,6 +2905,39 @@ public struct CryptoFile: Codable {
|
|||||||
public static func plain(_ f: String) -> CryptoFile {
|
public static func plain(_ f: String) -> CryptoFile {
|
||||||
CryptoFile(filePath: f, cryptoArgs: nil)
|
CryptoFile(filePath: f, cryptoArgs: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func decryptToTmpFile(_ filesToDelete: inout Set<URL>) async -> URL? {
|
||||||
|
if let cfArgs = cryptoArgs {
|
||||||
|
let url = getAppFilePath(filePath)
|
||||||
|
let tempUrl = getTempFilesDirectory().appendingPathComponent(filePath)
|
||||||
|
_ = filesToDelete.insert(tempUrl)
|
||||||
|
do {
|
||||||
|
try decryptCryptoFile(fromPath: url.path, cryptoArgs: cfArgs, toPath: tempUrl.path)
|
||||||
|
return tempUrl
|
||||||
|
} catch {
|
||||||
|
logger.error("Error decrypting file: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func decryptedGet() -> URL? {
|
||||||
|
let decrypted = CryptoFile.decryptedUrls[filePath]
|
||||||
|
return if let decrypted = decrypted, FileManager.default.fileExists(atPath: decrypted.path) { decrypted } else { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func decryptedGetOrCreate(_ filesToDelete: inout Set<URL>) async -> URL? {
|
||||||
|
if let decrypted = decryptedGet() {
|
||||||
|
return decrypted
|
||||||
|
} else if let decrypted = await decryptToTmpFile(&filesToDelete) {
|
||||||
|
CryptoFile.decryptedUrls[filePath] = decrypted
|
||||||
|
return decrypted
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var decryptedUrls = Dictionary<String, URL>()
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct CryptoFileArgs: Codable {
|
public struct CryptoFileArgs: Codable {
|
||||||
|
Loading…
Reference in New Issue
Block a user