diff --git a/Linphone/Assets.xcassets/lock-key.imageset/Contents.json b/Linphone/Assets.xcassets/lock-key.imageset/Contents.json new file mode 100644 index 000000000..309129c73 --- /dev/null +++ b/Linphone/Assets.xcassets/lock-key.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "lock-key.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/lock-key.imageset/lock-key.svg b/Linphone/Assets.xcassets/lock-key.imageset/lock-key.svg new file mode 100644 index 000000000..e033ef858 --- /dev/null +++ b/Linphone/Assets.xcassets/lock-key.imageset/lock-key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/security.imageset/Contents.json b/Linphone/Assets.xcassets/security.imageset/Contents.json new file mode 100644 index 000000000..ef6708f15 --- /dev/null +++ b/Linphone/Assets.xcassets/security.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "security.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/security.imageset/security.svg b/Linphone/Assets.xcassets/security.imageset/security.svg new file mode 100644 index 000000000..00fa4a741 --- /dev/null +++ b/Linphone/Assets.xcassets/security.imageset/security.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Linphone/Assets.xcassets/shield-warning.imageset/Contents.json b/Linphone/Assets.xcassets/shield-warning.imageset/Contents.json new file mode 100644 index 000000000..84d820c30 --- /dev/null +++ b/Linphone/Assets.xcassets/shield-warning.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "shield-warning.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/shield-warning.imageset/shield-warning.svg b/Linphone/Assets.xcassets/shield-warning.imageset/shield-warning.svg new file mode 100644 index 000000000..dae911b15 --- /dev/null +++ b/Linphone/Assets.xcassets/shield-warning.imageset/shield-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index c61bc684d..5730da51f 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -204,9 +204,6 @@ }, "Appel" : { - }, - "Appel chiffré de bout en bout" : { - }, "assistant_account_create" : { "localizations" : { @@ -421,6 +418,278 @@ }, "Call transfer failed!" : { + }, + "call_action_hang_up" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hang up" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raccrocher" + } + } + } + }, + "call_dialog_zrtp_security_alert_message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This call confidentiality may be compromise!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La confidentialité de votre appel peut être compromise !" + } + } + } + }, + "call_dialog_zrtp_security_alert_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Security alert" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerte de sécurité" + } + } + } + }, + "call_dialog_zrtp_security_alert_try_again" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try again" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer" + } + } + } + }, + "call_dialog_zrtp_validate_trust_letters_do_not_match" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nothing matches" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune correspondance" + } + } + } + }, + "call_dialog_zrtp_validate_trust_local_code_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your code:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre code :" + } + } + } + }, + "call_dialog_zrtp_validate_trust_message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "For your safety, we need to authenticate your correspondent device.
Please exchange your codes:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pour garantir le chiffrement, nous avons besoin d’authentifier l’appareil de votre correspondant.
Veuillez échanger vos codes :" + } + } + } + }, + "call_dialog_zrtp_validate_trust_remote_code_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Correspondent code:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Code correspondant :" + } + } + } + }, + "call_dialog_zrtp_validate_trust_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validate the device" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérification de sécurité" + } + } + } + }, + "call_dialog_zrtp_validate_trust_warning_message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "For your safety, we need to re-authenticate your correspondent device.
Please re-exchange your codes:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pour garantir le chiffrement, nous avons besoin de réauthentifier l’appareil de votre correspondant.
Veuillez ré-échanger vos codes :" + } + } + } + }, + "call_not_encrypted" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Call is not encrypted" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appel non chiffré" + } + } + } + }, + "call_srtp_point_to_point_encrypted" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Point-to-point encrypted by SRTP" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appel chiffré de point à point" + } + } + } + }, + "call_waiting_for_encryption_info" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waiting for encryption…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En attente du chiffrement…" + } + } + } + }, + "call_zrtp_end_to_end_encrypted" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "End-to-end encrypted by ZRTP" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appel chiffré de bout en bout" + } + } + } + }, + "call_zrtp_sas_validation_required" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validation required" + } + }, + "fr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Vérification nécessaire" + } + } + } + }, + "call_zrtp_sas_validation_skip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passer" + } + } + } }, "Calls" : { @@ -889,21 +1158,9 @@ }, "Joining..." : { - }, - "Key" : { - "extractionState" : "manual" - }, - "Key 1" : { - "extractionState" : "manual" - }, - "Key 2" : { - "extractionState" : "manual" }, "Last name" : { - }, - "Letters don't match!" : { - }, "Linphone" : { @@ -1101,9 +1358,6 @@ }, "Resuming" : { - }, - "Say %@ and click on the letters given by your correspondent:" : { - }, "Say something..." : { @@ -1283,9 +1537,6 @@ }, "Username error" : { - }, - "Validate the device" : { - }, "Vidéo" : { diff --git a/Linphone/TelecomManager/ProviderDelegate.swift b/Linphone/TelecomManager/ProviderDelegate.swift index 7a56be324..727c81dcb 100644 --- a/Linphone/TelecomManager/ProviderDelegate.swift +++ b/Linphone/TelecomManager/ProviderDelegate.swift @@ -225,7 +225,12 @@ extension ProviderDelegate: CXProviderDelegate { DispatchQueue.main.async { if UIApplication.shared.applicationState != .active { TelecomManager.shared.backgroundContextCall = call - TelecomManager.shared.backgroundContextCameraIsEnabled = call?.params?.videoEnabled == true || call?.callLog?.wasConference() == true + if call?.callLog != nil { + TelecomManager.shared.backgroundContextCameraIsEnabled = call?.params?.videoEnabled == true || call?.callLog?.wasConference() == true + } else { + TelecomManager.shared.backgroundContextCameraIsEnabled = call?.params?.videoEnabled == true + } + if #available(iOS 16.0, *) { if call?.cameraEnabled == true { call?.cameraEnabled = AVCaptureSession().isMultitaskingCameraAccessSupported diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 1cb34c1dc..59f282a6d 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -44,6 +44,7 @@ class TelecomManager: ObservableObject { @Published var callInProgress: Bool = false @Published var callDisplayed: Bool = true @Published var callStarted: Bool = false + @Published var isNotVerifiedCounter: Int = 0 @Published var outgoingCallStarted: Bool = false @Published var remoteConfVideo: Bool = false @Published var isRecordingByRemote: Bool = false @@ -269,6 +270,7 @@ class TelecomManager: ObservableObject { DispatchQueue.main.async { self.outgoingCallStarted = true self.callStarted = true + self.isNotVerifiedCounter = 0 if self.callInProgress == false { withAnimation { self.callInProgress = true @@ -316,6 +318,7 @@ class TelecomManager: ObservableObject { DispatchQueue.main.async { self.callStarted = true + self.isNotVerifiedCounter = 0 } } catch { Log.error("accept call failed \(error)") diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 2d74a788b..051f0f8e6 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -177,11 +177,19 @@ struct CallView: View { } if callViewModel.zrtpPopupDisplayed == true { - ZRTPPopup(callViewModel: callViewModel) - .background(.black.opacity(0.65)) - .onTapGesture { - callViewModel.zrtpPopupDisplayed = false - } + if idiom != .pad + && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) + && buttonSize != 45 { + ZRTPPopup(callViewModel: callViewModel, resizeView: 1.5) + .background(.black.opacity(0.65)) + .frame(maxHeight: geo.size.height) + } else { + ZRTPPopup(callViewModel: callViewModel, resizeView: buttonSize == 45 ? 1.5 : 1) + .background(.black.opacity(0.65)) + .frame(maxHeight: geo.size.height) + } } if telecomManager.remainingCall { @@ -226,7 +234,7 @@ struct CallView: View { } Text(callViewModel.displayName) - .default_text_style_white_800(styleSize: 16) + .default_text_style_white_800(styleSize: 16) if !telecomManager.outgoingCallStarted && telecomManager.callInProgress { Text("|") @@ -281,26 +289,114 @@ struct CallView: View { .frame(height: 40) .zIndex(1) - if callViewModel.isMediaEncrypted { - HStack { - Image("lock_simple") - .resizable() - .frame(width: 15, height: 15, alignment: .leading) - .padding(.leading, 50) - .padding(.top, 35) - - Text("Appel chiffré de bout en bout") - .foregroundStyle(Color.blueInfo500) - .default_text_style_white(styleSize: 12) - .padding(.top, 35) - - Spacer() + if !telecomManager.outgoingCallStarted && telecomManager.callInProgress { + if callViewModel.isMediaEncrypted && callViewModel.isRemoteDeviceTrusted && callViewModel.isZrtp { + HStack { + Image("lock-key") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.blueInfo500) + .frame(width: 15, height: 15, alignment: .leading) + .padding(.leading, 50) + .padding(.top, 35) + + Text("call_zrtp_end_to_end_encrypted") + .foregroundStyle(Color.blueInfo500) + .default_text_style_white(styleSize: 12) + .padding(.top, 35) + + Spacer() + } + .onTapGesture { + mediaEncryptedSheet = true + } + .frame(height: 40) + .zIndex(1) + } else if callViewModel.isMediaEncrypted && !callViewModel.isZrtp { + HStack { + Image("lock_simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.blueInfo500) + .frame(width: 15, height: 15, alignment: .leading) + .padding(.leading, 50) + .padding(.top, 35) + + Text("call_srtp_point_to_point_encrypted") + .foregroundStyle(Color.blueInfo500) + .default_text_style_white(styleSize: 12) + .padding(.top, 35) + + Spacer() + } + .onTapGesture { + mediaEncryptedSheet = true + } + .frame(height: 40) + .zIndex(1) + } else if callViewModel.isMediaEncrypted && (!callViewModel.isRemoteDeviceTrusted && callViewModel.isZrtp) || callViewModel.cacheMismatch { + HStack { + Image("warning-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeWarning600) + .frame(width: 15, height: 15, alignment: .leading) + .padding(.leading, 50) + .padding(.top, 35) + + Text("call_zrtp_sas_validation_required") + .foregroundStyle(Color.orangeWarning600) + .default_text_style_white(styleSize: 12) + .padding(.top, 35) + + Spacer() + } + .onTapGesture { + mediaEncryptedSheet = true + } + .frame(height: 40) + .zIndex(1) + } else if callViewModel.isNotEncrypted { + HStack { + Image("lock_simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 15, height: 15, alignment: .leading) + .padding(.leading, 50) + .padding(.top, 35) + + Text("call_not_encrypted") + .foregroundStyle(.white) + .default_text_style_white(styleSize: 12) + .padding(.top, 35) + + Spacer() + } + .onTapGesture { + mediaEncryptedSheet = true + } + .frame(height: 40) + .zIndex(1) + } else { + HStack { + ProgressView() + .controlSize(.mini) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(width: 15, height: 15, alignment: .leading) + .padding(.leading, 50) + .padding(.top, 35) + + Text("call_waiting_for_encryption_info") + .foregroundStyle(.white) + .default_text_style_white(styleSize: 12) + .padding(.top, 35) + + Spacer() + } + .frame(height: 40) + .zIndex(1) } - .onTapGesture { - mediaEncryptedSheet = true - } - .frame(height: 40) - .zIndex(1) } } } diff --git a/Linphone/UI/Call/Fragments/ZRTPPopup.swift b/Linphone/UI/Call/Fragments/ZRTPPopup.swift index 332803d22..3c27f8df4 100644 --- a/Linphone/UI/Call/Fragments/ZRTPPopup.swift +++ b/Linphone/UI/Call/Fragments/ZRTPPopup.swift @@ -27,123 +27,367 @@ struct ZRTPPopup: View { @ObservedObject var callViewModel: CallViewModel + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State private var orientation = UIDevice.current.orientation + + var resizeView: CGFloat + var body: some View { + if callViewModel.isNotVerified { + alertZRTP + } else { + popupZRTP + } + } + + var popupZRTP: some View { GeometryReader { geometry in VStack(alignment: .leading) { - Text("Validate the device") - .default_text_style_600(styleSize: 20) - - Text("Say \(callViewModel.upperCaseAuthTokenToRead) and click on the letters given by your correspondent:") - .default_text_style(styleSize: 15) - .padding(.bottom, 20) - - HStack(spacing: 25) { - Spacer() - - HStack(alignment: .center) { - Text(callViewModel.letters1) - .default_text_style(styleSize: 30) - .frame(width: 60, height: 60) - } - .padding(10) - .background(Color.grayMain2c200) - .cornerRadius(40) - .onTapGesture { - callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters1) - callViewModel.zrtpPopupDisplayed = false - } - - HStack(alignment: .center) { - Text(callViewModel.letters2) - .default_text_style(styleSize: 30) - .frame(width: 60, height: 60) - } - .padding(10) - .background(Color.grayMain2c200) - .cornerRadius(40) - .onTapGesture { - callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters2) - callViewModel.zrtpPopupDisplayed = false - } - - Spacer() - } - .padding(.bottom, 20) - - HStack(spacing: 25) { - Spacer() - - HStack(alignment: .center) { - Text(callViewModel.letters3) - .default_text_style(styleSize: 30) - .frame(width: 60, height: 60) - } - .padding(10) - .background(Color.grayMain2c200) - .cornerRadius(40) - .onTapGesture { - callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters3) - callViewModel.zrtpPopupDisplayed = false - } - - HStack(alignment: .center) { - Text(callViewModel.letters4) - .default_text_style(styleSize: 30) - .frame(width: 60, height: 60) - } - .padding(10) - .background(Color.grayMain2c200) - .cornerRadius(40) - .onTapGesture { - callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters4) - callViewModel.zrtpPopupDisplayed = false - } - - Spacer() - } - .padding(.bottom, 20) - - HStack { - Text("Skip") - .underline() - .tint(Color.grayMain2c600) - .default_text_style_600(styleSize: 15) - .foregroundStyle(Color.grayMain2c500) - } - .frame(maxWidth: .infinity) - .padding(.bottom, 30) - .onTapGesture { - callViewModel.skipZrtpAuthentication() - callViewModel.zrtpPopupDisplayed = false - } - - Button(action: { - callViewModel.updateZrtpSas(authTokenClicked: "") - callViewModel.zrtpPopupDisplayed = false - }, label: { - Text("Letters don't match!") - .default_text_style_orange_600(styleSize: 20) - .frame(height: 35) + ZStack(alignment: .top, content: { + HStack { + Spacer() + + VStack { + Image("security") + .resizable() + .frame(width: 20, height: 20, alignment: .leading) + + Text("call_dialog_zrtp_validate_trust_title") + .default_text_style_white_700(styleSize: 16 / resizeView) + } .frame(maxWidth: .infinity) + + Spacer() + } + .padding(.top, 15) + .padding(.bottom, 2) + + HStack { + Spacer() + HStack { + Text("call_zrtp_sas_validation_skip") + .underline() + .tint(.white) + .default_text_style_white_600(styleSize: 16 / resizeView) + .foregroundStyle(.white) + } + .onTapGesture { + callViewModel.skipZrtpAuthentication() + callViewModel.zrtpPopupDisplayed = false + } + } + .padding(.top, 10 / resizeView) + .padding(.trailing, 15 / resizeView) }) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .cornerRadius(60) - .overlay( - RoundedRectangle(cornerRadius: 60) - .inset(by: 0.5) - .stroke(Color.orangeMain500, lineWidth: 1) - ) - .padding(.bottom) + + VStack(alignment: .center) { + VStack { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + HStack { + Text("call_dialog_zrtp_validate_trust_message") + .default_text_style(styleSize: 16 / resizeView) + .multilineTextAlignment(.center) + .padding(.bottom, 10 / resizeView) + + VStack { + Text("call_dialog_zrtp_validate_trust_local_code_label") + .default_text_style(styleSize: 16 / resizeView) + .multilineTextAlignment(.center) + + Text(!callViewModel.upperCaseAuthTokenToRead.isEmpty ? callViewModel.upperCaseAuthTokenToRead : "ZZ") + .default_text_style_700(styleSize: 22 / resizeView) + .padding(.bottom, 20 / resizeView) + } + } + } else { + Text(callViewModel.cacheMismatch ? "call_dialog_zrtp_validate_trust_warning_message" : "call_dialog_zrtp_validate_trust_message") + .default_text_style(styleSize: 16 / resizeView) + .multilineTextAlignment(.center) + .padding(.bottom, 10 / resizeView) + + Text("call_dialog_zrtp_validate_trust_local_code_label") + .default_text_style(styleSize: 16 / resizeView) + .multilineTextAlignment(.center) + + Text(!callViewModel.upperCaseAuthTokenToRead.isEmpty ? callViewModel.upperCaseAuthTokenToRead : "ZZ") + .default_text_style_800(styleSize: 22 / resizeView) + } + } + .padding(.bottom, 5) + + VStack { + Text("call_dialog_zrtp_validate_trust_remote_code_label") + .default_text_style(styleSize: 16 / resizeView) + .multilineTextAlignment(.center) + .padding(.top, 15 / resizeView) + .padding(.bottom, 10 / resizeView) + + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + HStack(spacing: 30) { + HStack(alignment: .center) { + Text(callViewModel.letters1) + .default_text_style(styleSize: 24 / resizeView) + .frame(width: 45 / resizeView, height: 45 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters1) + callViewModel.zrtpPopupDisplayed = false + } + + HStack(alignment: .center) { + Text(callViewModel.letters2) + .default_text_style(styleSize: 24 / resizeView) + .frame(width: 45 / resizeView, height: 45 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters2) + callViewModel.zrtpPopupDisplayed = false + } + + HStack(alignment: .center) { + Text(callViewModel.letters3) + .default_text_style(styleSize: 24 / resizeView) + .frame(width: 45 / resizeView, height: 45 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters3) + callViewModel.zrtpPopupDisplayed = false + } + + HStack(alignment: .center) { + Text(callViewModel.letters4) + .default_text_style(styleSize: 24 / resizeView) + .frame(width: 45 / resizeView, height: 45 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters4) + callViewModel.zrtpPopupDisplayed = false + } + } + .padding(.horizontal, 40 / resizeView) + .padding(.bottom, 20 / resizeView) + } else { + HStack(spacing: 30) { + HStack(alignment: .center) { + Text(callViewModel.letters1) + .default_text_style(styleSize: 34 / resizeView) + .frame(width: 60 / resizeView, height: 60 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters1) + callViewModel.zrtpPopupDisplayed = false + } + + HStack(alignment: .center) { + Text(callViewModel.letters2) + .default_text_style(styleSize: 34 / resizeView) + .frame(width: 60 / resizeView, height: 60 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters2) + callViewModel.zrtpPopupDisplayed = false + } + } + .padding(.horizontal, 40 / resizeView) + .padding(.bottom, 20 / resizeView) + + HStack(spacing: 30) { + HStack(alignment: .center) { + Text(callViewModel.letters3) + .default_text_style(styleSize: 34 / resizeView) + .frame(width: 60 / resizeView, height: 60 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters3) + callViewModel.zrtpPopupDisplayed = false + } + + HStack(alignment: .center) { + Text(callViewModel.letters4) + .default_text_style(styleSize: 34 / resizeView) + .frame(width: 60 / resizeView, height: 60 / resizeView) + } + .padding(10 / resizeView) + .background(.white) + .clipShape(Circle()) + .shadow(color: .gray.opacity(0.4), radius: 4) + .onTapGesture { + callViewModel.updateZrtpSas(authTokenClicked: callViewModel.letters4) + callViewModel.zrtpPopupDisplayed = false + } + } + .padding(.horizontal, 40 / resizeView) + .padding(.bottom, 20 / resizeView) + } + } + .padding(.horizontal, 10 / resizeView) + .padding(.bottom, 10 / resizeView) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .inset(by: 0.5) + .stroke(Color.grayMain2c200, lineWidth: 1) + ) + .padding(.bottom, 10 / resizeView) + + Button(action: { + callViewModel.updateZrtpSas(authTokenClicked: "") + callViewModel.zrtpPopupDisplayed = false + }, label: { + Text("call_dialog_zrtp_validate_trust_letters_do_not_match") + .foregroundStyle(Color.redDanger500) + .default_text_style_orange_600(styleSize: 20 / resizeView) + .frame(height: 35 / resizeView) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20 / resizeView) + .padding(.vertical, 10 / resizeView) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.redDanger500, lineWidth: 1) + ) + .padding(.bottom) + } + .padding(.top, 20 / resizeView) + .padding(.horizontal, 20 / resizeView) + .background(.white) + .cornerRadius(20) } - .padding(.horizontal, 20) - .padding(.vertical, 20) - .background(.white) + .background(callViewModel.cacheMismatch ? Color.orangeWarning600 : Color.blueInfo500) .cornerRadius(20) - .padding(.horizontal) + .padding(.horizontal, 2) .frame(maxHeight: .infinity) - .shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2) - .frame(maxWidth: sharedMainViewModel.maxWidth) + .shadow(color: callViewModel.cacheMismatch ? Color.orangeWarning600 : Color.blueInfo500, radius: 0, x: 0, y: 2) + .frame(maxWidth: sharedMainViewModel.maxWidth * 1.2) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + .onAppear { + callViewModel.remoteAuthenticationTokens() + } + } + } + + var alertZRTP: some View { + GeometryReader { geometry in + VStack(alignment: .leading) { + ZStack(alignment: .top, content: { + HStack { + Spacer() + + VStack { + Image("shield-warning") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + + Text("call_dialog_zrtp_security_alert_title") + .default_text_style_white_700(styleSize: 16 / resizeView) + } + .frame(maxWidth: .infinity) + + Spacer() + } + .padding(.top, 15) + .padding(.bottom, 2) + }) + + VStack(alignment: .center) { + VStack { + Text("call_dialog_zrtp_security_alert_message") + .default_text_style(styleSize: 16 / resizeView) + .multilineTextAlignment(.center) + .padding(.bottom, 10 / resizeView) + } + .padding(.bottom, 5) + + if telecomManager.isNotVerifiedCounter <= 1 { + Button(action: { + callViewModel.isNotVerified = false + }, label: { + Text("call_dialog_zrtp_security_alert_try_again") + .foregroundStyle(Color.redDanger500) + .default_text_style_orange_600(styleSize: 20 / resizeView) + .frame(height: 35 / resizeView) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20 / resizeView) + .padding(.vertical, 10 / resizeView) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.redDanger500, lineWidth: 1) + ) + .padding(.bottom) + } + + Button(action: { + callViewModel.terminateCall() + }, label: { + HStack { + Image("phone-disconnect") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 20, height: 20) + + Text("call_action_hang_up") + .default_text_style_white_600(styleSize: 20) + .frame(height: 35) + } + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20 / resizeView) + .padding(.vertical, 10 / resizeView) + .background(Color.redDanger500) + .cornerRadius(60) + .padding(.bottom) + } + .padding(.top, 20 / resizeView) + .padding(.horizontal, 20 / resizeView) + .background(.white) + .cornerRadius(20) + } + .background(Color.redDanger500) + .cornerRadius(20) + .padding(.horizontal, 2) + .frame(maxHeight: .infinity) + .shadow(color: Color.redDanger500, radius: 0, x: 0, y: 2) + .frame(maxWidth: sharedMainViewModel.maxWidth * 1.2) .position(x: geometry.size.width / 2, y: geometry.size.height / 2) .onAppear { callViewModel.remoteAuthenticationTokens() @@ -153,5 +397,5 @@ struct ZRTPPopup: View { } #Preview { - ZRTPPopup(callViewModel: CallViewModel()) + ZRTPPopup(callViewModel: CallViewModel(), resizeView: 1) } diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 741d58cf4..6fcdf3ba8 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -42,8 +42,11 @@ class CallViewModel: ObservableObject { @Published var upperCaseAuthTokenToRead = "" @Published var upperCaseAuthTokenToListen = "" @Published var isMediaEncrypted: Bool = false - @Published var isZrtpPq: Bool = false + @Published var isNotEncrypted: Bool = false + @Published var isZrtp: Bool = false @Published var isRemoteDeviceTrusted: Bool = false + @Published var cacheMismatch: Bool = false + @Published var isNotVerified: Bool = false @Published var selectedCall: Call? @Published var isTransferInsteadCall: Bool = false @Published var isOneOneCall: Bool = false @@ -125,14 +128,14 @@ class CallViewModel: ObservableObject { } var isMediaEncryptedTmp = false - var isZrtpPqTmp = false + var isZrtpTmp = false if self.currentCall != nil && self.currentCall!.currentParams != nil { if self.currentCall!.currentParams!.mediaEncryption == .ZRTP || self.currentCall!.currentParams!.mediaEncryption == .SRTP || self.currentCall!.currentParams!.mediaEncryption == .DTLS { isMediaEncryptedTmp = true - isZrtpPqTmp = self.currentCall!.currentParams!.mediaEncryption == .ZRTP + isZrtpTmp = self.currentCall!.currentParams!.mediaEncryption == .ZRTP } } @@ -200,6 +203,9 @@ class CallViewModel: ObservableObject { self.zrtpPopupDisplayed = false self.upperCaseAuthTokenToRead = "" self.upperCaseAuthTokenToListen = "" + self.isNotVerified = false + + self.updateEncryption() self.isConference = false self.participantList = [] self.activeSpeakerParticipant = nil @@ -209,7 +215,9 @@ class CallViewModel: ObservableObject { self.videoDisplayed = videoDisplayedTmp self.isOneOneCall = isOneOneCallTmp self.isMediaEncrypted = isMediaEncryptedTmp - self.isZrtpPq = isZrtpPqTmp + self.isNotEncrypted = false + self.isZrtp = isZrtpTmp + self.cacheMismatch = cacheMismatchFlag self.getCallsList() @@ -228,21 +236,35 @@ class CallViewModel: ObservableObject { }) self.callSuscriptions.insert(self.currentCall!.publisher?.onStatsUpdated?.postOnCoreQueue {(cbVal: (call: Call, stats: CallStats)) in - if self.currentCall != nil { - DispatchQueue.main.async { + DispatchQueue.main.async { + if self.currentCall != nil { self.callStatsModel.update(call: self.currentCall!, stats: cbVal.stats) } } }) - self.callSuscriptions.insert( self.currentCall!.publisher?.onAuthenticationTokenVerified?.postOnCoreQueue {(call: Call, verified: Bool) in Log.warn("[CallViewModel][ZRTPPopup] Notified that authentication token is \(verified ? "verified" : "not verified!")") - - self.updateEncryption() - if self.currentCall != nil { - self.callMediaEncryptionModel.update(call: self.currentCall!) + if verified { + self.updateEncryption() + if self.currentCall != nil { + self.callMediaEncryptionModel.update(call: self.currentCall!) + } + } else { + if self.telecomManager.isNotVerifiedCounter == 0 { + DispatchQueue.main.async { + self.isNotVerified = true + self.telecomManager.isNotVerifiedCounter += 1 + } + self.showZrtpSasDialogIfPossible() + } else { + DispatchQueue.main.async { + self.isNotVerified = true + self.telecomManager.isNotVerifiedCounter += 1 + self.zrtpPopupDisplayed = true + } + } } } ) @@ -641,6 +663,7 @@ class CallViewModel: ObservableObject { telecomManager.callInProgress = true telecomManager.callDisplayed = true telecomManager.callStarted = true + telecomManager.isNotVerifiedCounter = 0 } coreContext.doOnCoreQueue { core in @@ -881,18 +904,20 @@ class CallViewModel: ObservableObject { coreContext.doOnCoreQueue { core in if core.currentCall != nil { let tokens = core.currentCall!.remoteAuthenticationTokens - DispatchQueue.main.async { - self.letters1 = tokens[0] - self.letters2 = tokens[1] - self.letters3 = tokens[2] - self.letters4 = tokens[3] + if !tokens.isEmpty { + DispatchQueue.main.async { + self.letters1 = tokens[0] + self.letters2 = tokens[1] + self.letters3 = tokens[2] + self.letters4 = tokens[3] + } } } } } private func updateEncryption() { - coreContext.doOnCoreQueue { core in + coreContext.doOnCoreQueue { _ in if self.currentCall != nil && self.currentCall!.currentParams != nil { switch self.currentCall!.currentParams!.mediaEncryption { case MediaEncryption.ZRTP: @@ -918,12 +943,14 @@ class CallViewModel: ObservableObject { */ // When Post Quantum is available, ZRTP is Post Quantum - let isZrtpPqTmp = Core.getPostQuantumAvailable + let isZrtpPQTmp = Core.getPostQuantumAvailable DispatchQueue.main.async { self.isRemoteDeviceTrusted = isRemoteDeviceTrustedTmp self.isMediaEncrypted = true - self.isZrtpPq = isZrtpPqTmp + self.isZrtp = true + self.cacheMismatch = cacheMismatchFlag + self.isNotEncrypted = false if isDeviceTrusted { ToastViewModel.shared.toastMessage = "Info_call_securised" @@ -938,12 +965,18 @@ class CallViewModel: ObservableObject { case MediaEncryption.SRTP, MediaEncryption.DTLS: DispatchQueue.main.async { self.isMediaEncrypted = true - self.isZrtpPq = false + self.isZrtp = false + self.isNotEncrypted = false } - default: + case MediaEncryption.None: DispatchQueue.main.async { self.isMediaEncrypted = false - self.isZrtpPq = false + self.isZrtp = false + if self.currentCall!.state == .StreamsRunning { + self.isNotEncrypted = true + } else { + self.isNotEncrypted = false + } } } } @@ -971,26 +1004,28 @@ class CallViewModel: ObservableObject { let mySubstringSuffix = upperCaseAuthToken.suffix(2) - switch self.currentCall!.dir { - case Call.Dir.Incoming: - self.upperCaseAuthTokenToRead = String(mySubstringPrefix) - self.upperCaseAuthTokenToListen = String(mySubstringSuffix) - default: - self.upperCaseAuthTokenToRead = String(mySubstringSuffix) - self.upperCaseAuthTokenToListen = String(mySubstringPrefix) + DispatchQueue.main.async { + switch self.currentCall!.dir { + case Call.Dir.Incoming: + self.upperCaseAuthTokenToRead = String(mySubstringPrefix) + self.upperCaseAuthTokenToListen = String(mySubstringSuffix) + default: + self.upperCaseAuthTokenToRead = String(mySubstringSuffix) + self.upperCaseAuthTokenToListen = String(mySubstringPrefix) + } + + self.zrtpPopupDisplayed = true } - - self.zrtpPopupDisplayed = true } } func transferClicked() { coreContext.doOnCoreQueue { core in - var callToTransferTo = core.calls.last { call in + let callToTransferTo = core.calls.last { call in call.state == Call.State.Paused && call.callLog?.callId != self.currentCall?.callLog?.callId } - if (callToTransferTo == nil) { + if callToTransferTo == nil { Log.error( "[CallViewModel] Couldn't find a call in Paused state to transfer current call to" ) diff --git a/Linphone/Utils/Extensions/AccountExtension.swift b/Linphone/Utils/Extensions/AccountExtension.swift index 44be51e12..7ae58904b 100644 --- a/Linphone/Utils/Extensions/AccountExtension.swift +++ b/Linphone/Utils/Extensions/AccountExtension.swift @@ -35,6 +35,10 @@ extension Account { } static func == (lhs: Account, rhs: Account) -> Bool { - return lhs.params?.identityAddress?.asString() == rhs.params?.identityAddress?.asString() + if lhs.params != nil && lhs.params?.identityAddress != nil && rhs.params != nil && rhs.params?.identityAddress != nil { + return lhs.params?.identityAddress?.asString() == rhs.params?.identityAddress?.asString() + } else { + return false + } } }