2022-09-07 12:49:41 +01:00
//
// D a t a b a s e E n c r y p t i o n V i e w . s w i f t
// S i m p l e X ( i O S )
//
// C r e a t e d b y E v g e n y o n 0 4 / 0 9 / 2 0 2 2 .
// C o p y r i g h t © 2 0 2 2 S i m p l e X C h a t . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
import SimpleXChat
enum DatabaseEncryptionAlert : Identifiable {
case keychainRemoveKey
case encryptDatabaseSaved
case encryptDatabase
case changeDatabaseKeySaved
case changeDatabaseKey
case databaseEncrypted
case currentPassphraseError
2022-11-25 13:50:26 +00:00
case error ( title : LocalizedStringKey , error : LocalizedStringKey = " " )
2022-09-07 12:49:41 +01:00
var id : String {
switch self {
case . keychainRemoveKey : return " keychainRemoveKey "
case . encryptDatabaseSaved : return " encryptDatabaseSaved "
case . encryptDatabase : return " encryptDatabase "
case . changeDatabaseKeySaved : return " changeDatabaseKeySaved "
case . changeDatabaseKey : return " changeDatabaseKey "
case . databaseEncrypted : return " databaseEncrypted "
case . currentPassphraseError : return " currentPassphraseError "
case let . error ( title , _ ) : return " error \( title ) "
}
}
}
struct DatabaseEncryptionView : View {
@ EnvironmentObject private var m : ChatModel
2024-07-03 22:42:13 +01:00
@ EnvironmentObject private var theme : AppTheme
2022-09-08 17:36:16 +01:00
@ Binding var useKeychain : Bool
2024-03-11 21:17:28 +07:00
var migration : Bool
2022-09-07 12:49:41 +01:00
@ State private var alert : DatabaseEncryptionAlert ? = nil
@ State private var progressIndicator = false
@ State private var useKeychainToggle = storeDBPassphraseGroupDefault . get ( )
@ State private var initialRandomDBPassphrase = initialRandomDBPassphraseGroupDefault . get ( )
2023-04-12 12:22:55 +02:00
@ State private var storedKey = kcDatabasePassword . get ( ) != nil
2022-09-07 12:49:41 +01:00
@ State private var currentKey = " "
@ State private var newKey = " "
@ State private var confirmNewKey = " "
@ State private var currentKeyShown = false
2024-11-30 23:29:27 +07:00
let stopChatRunBlockStartChat : ( Binding < Bool > , @ escaping ( ) async throws -> Bool ) -> Void
2022-09-07 12:49:41 +01:00
var body : some View {
ZStack {
2024-03-11 21:17:28 +07:00
List {
if migration {
chatStoppedView ( )
}
databaseEncryptionView ( )
}
2022-09-07 12:49:41 +01:00
if progressIndicator {
ProgressView ( ) . scaleEffect ( 2 )
}
}
}
private func databaseEncryptionView ( ) -> some View {
2024-03-11 21:17:28 +07:00
Section {
2024-07-03 22:42:13 +01:00
settingsRow ( storedKey ? " key.fill " : " key " , color : storedKey ? . green : theme . colors . secondary ) {
2024-03-11 21:17:28 +07:00
Toggle ( " Save passphrase in Keychain " , isOn : $ useKeychainToggle )
2022-09-07 12:49:41 +01:00
. onChange ( of : useKeychainToggle ) { _ in
if useKeychainToggle {
setUseKeychain ( true )
2024-03-11 21:17:28 +07:00
} else if storedKey && ! migration {
// D o n ' t s h o w i n m i g r a t i o n p r o c e s s s i n c e i t w i l l r e m o v e t h e k e y a f t e r s u c c e s s f u l l e n c r y p t i o n
2022-09-07 12:49:41 +01:00
alert = . keychainRemoveKey
} else {
setUseKeychain ( false )
}
}
2024-03-11 21:17:28 +07:00
. disabled ( initialRandomDBPassphrase && ! migration )
}
2022-09-07 12:49:41 +01:00
2024-03-11 21:17:28 +07:00
if ! initialRandomDBPassphrase && m . chatDbEncrypted = = true {
PassphraseField ( key : $ currentKey , placeholder : " Current passphrase… " , valid : validKey ( currentKey ) )
}
2022-09-07 12:49:41 +01:00
2024-03-11 21:17:28 +07:00
PassphraseField ( key : $ newKey , placeholder : " New passphrase… " , valid : validKey ( newKey ) , showStrength : true )
PassphraseField ( key : $ confirmNewKey , placeholder : " Confirm new passphrase… " , valid : confirmNewKey = = " " || newKey = = confirmNewKey )
2022-09-07 12:49:41 +01:00
2024-07-03 22:42:13 +01:00
settingsRow ( " lock.rotation " , color : theme . colors . secondary ) {
2024-03-11 21:17:28 +07:00
Button ( migration ? " Set passphrase " : " Update database passphrase " ) {
alert = currentKey = = " "
? ( useKeychain ? . encryptDatabaseSaved : . encryptDatabase )
: ( useKeychain ? . changeDatabaseKeySaved : . changeDatabaseKey )
2022-09-07 12:49:41 +01:00
}
2024-03-11 21:17:28 +07:00
}
. disabled (
( m . chatDbEncrypted = = true && currentKey = = " " ) ||
currentKey = = newKey ||
newKey != confirmNewKey ||
newKey = = " " ||
! validKey ( currentKey ) ||
! validKey ( newKey )
)
} header : {
Text ( migration ? " Database passphrase " : " " )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
2024-03-11 21:17:28 +07:00
} footer : {
VStack ( alignment : . leading , spacing : 16 ) {
if m . chatDbEncrypted = = false {
Text ( " Your chat database is not encrypted - set passphrase to encrypt it. " )
} else if useKeychain {
if storedKey {
Text ( " iOS Keychain is used to securely store passphrase - it allows receiving push notifications. " )
if initialRandomDBPassphrase && ! migration {
Text ( " Database is encrypted using a random passphrase, you can change it. " )
2022-09-07 12:49:41 +01:00
} else {
2024-03-11 21:17:28 +07:00
Text ( " **Please note**: you will NOT be able to recover or change passphrase if you lose it. " )
2022-09-07 12:49:41 +01:00
}
} else {
2024-03-11 21:17:28 +07:00
Text ( " iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. " )
}
} else {
Text ( " You have to enter passphrase every time the app starts - it is not stored on the device. " )
Text ( " **Please note**: you will NOT be able to recover or change passphrase if you lose it. " )
if m . notificationMode = = . instant && m . notificationPreview != . hidden && ! migration {
Text ( " **Warning**: Instant push notifications require passphrase saved in Keychain. " )
2022-09-07 12:49:41 +01:00
}
}
}
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
2024-03-11 21:17:28 +07:00
. padding ( . top , 1 )
. font ( . callout )
2022-09-07 12:49:41 +01:00
}
. onAppear {
2023-04-12 12:22:55 +02:00
if initialRandomDBPassphrase { currentKey = kcDatabasePassword . get ( ) ? ? " " }
2022-09-07 12:49:41 +01:00
}
2024-11-30 23:29:27 +07:00
. disabled ( progressIndicator )
2022-09-07 12:49:41 +01:00
. alert ( item : $ alert ) { item in databaseEncryptionAlert ( item ) }
}
2024-11-30 23:29:27 +07:00
private func encryptDatabaseAsync ( ) async -> Bool {
await MainActor . run {
progressIndicator = true
}
do {
encryptionStartedDefault . set ( true )
encryptionStartedAtDefault . set ( Date . now )
if ! m . chatDbChanged {
try apiSaveAppSettings ( settings : AppSettings . current . prepareForExport ( ) )
}
try await apiStorageEncryption ( currentKey : currentKey , newKey : newKey )
encryptionStartedDefault . set ( false )
initialRandomDBPassphraseGroupDefault . set ( false )
if migration {
storeDBPassphraseGroupDefault . set ( useKeychain )
}
if useKeychain {
if kcDatabasePassword . set ( newKey ) {
await resetFormAfterEncryption ( true )
await operationEnded ( . databaseEncrypted )
2022-09-07 12:49:41 +01:00
} else {
await resetFormAfterEncryption ( )
2024-11-30 23:29:27 +07:00
await operationEnded ( . error ( title : " Keychain error " , error : " Error saving passphrase to keychain " ) )
2022-09-07 12:49:41 +01:00
}
2024-11-30 23:29:27 +07:00
} else {
if migration {
removePassphraseFromKeyChain ( )
2022-09-07 12:49:41 +01:00
}
2024-11-30 23:29:27 +07:00
await resetFormAfterEncryption ( )
await operationEnded ( . databaseEncrypted )
}
return true
} catch let error {
2025-05-05 11:51:22 +01:00
if case . errorDatabase ( . errorExport ( . errorNotADatabase ) ) = error as ? ChatError {
2024-11-30 23:29:27 +07:00
await operationEnded ( . currentPassphraseError )
} else {
await operationEnded ( . error ( title : " Error encrypting database " , error : " \( responseError ( error ) ) " ) )
}
return false
}
}
private func encryptDatabase ( ) {
// i t w i l l t r y t o s t o p a n d s t a r t t h e c h a t i n c a s e o f : n o n - m i g r a t i o n & & s u c c e s s f u l e n c r y p t i o n . I n m i g r a t i o n t h e c h a t w i l l r e m a i n s t o p p e d
if migration {
Task {
await encryptDatabaseAsync ( )
}
} else {
stopChatRunBlockStartChat ( $ progressIndicator ) {
return await encryptDatabaseAsync ( )
2022-09-07 12:49:41 +01:00
}
}
}
private func resetFormAfterEncryption ( _ stored : Bool = false ) async {
await MainActor . run {
m . chatDbEncrypted = true
initialRandomDBPassphrase = false
currentKey = " "
newKey = " "
confirmNewKey = " "
storedKey = stored
}
}
private func setUseKeychain ( _ value : Bool ) {
useKeychain = value
2024-03-11 21:17:28 +07:00
// P o s t p o n e i t w h e n m i g r a t i n g t o t h e e n d o f e n c r y p t i o n p r o c e s s
if ! migration {
storeDBPassphraseGroupDefault . set ( value )
}
2022-09-07 12:49:41 +01:00
}
private func databaseEncryptionAlert ( _ alertItem : DatabaseEncryptionAlert ) -> Alert {
switch alertItem {
case . keychainRemoveKey :
return Alert (
title : Text ( " Remove passphrase from keychain? " ) ,
message : Text ( " Instant push notifications will be hidden! \n " ) + storeSecurelyDanger ( ) ,
primaryButton : . destructive ( Text ( " Remove " ) ) {
2024-03-11 21:17:28 +07:00
removePassphraseFromKeyChain ( )
2022-09-07 12:49:41 +01:00
} ,
secondaryButton : . cancel ( ) {
withAnimation { useKeychainToggle = true }
}
)
case . encryptDatabaseSaved :
return Alert (
title : Text ( " Encrypt database? " ) ,
message : Text ( " Database will be encrypted and the passphrase stored in the keychain. \n " ) + storeSecurelySaved ( ) ,
primaryButton : . default ( Text ( " Encrypt " ) ) { encryptDatabase ( ) } ,
secondaryButton : . cancel ( )
)
case . encryptDatabase :
return Alert (
title : Text ( " Encrypt database? " ) ,
message : Text ( " Database will be encrypted. \n " ) + storeSecurelyDanger ( ) ,
primaryButton : . destructive ( Text ( " Encrypt " ) ) { encryptDatabase ( ) } ,
secondaryButton : . cancel ( )
)
case . changeDatabaseKeySaved :
return Alert (
title : Text ( " Change database passphrase? " ) ,
message : Text ( " Database encryption passphrase will be updated and stored in the keychain. \n " ) + storeSecurelySaved ( ) ,
primaryButton : . default ( Text ( " Update " ) ) { encryptDatabase ( ) } ,
secondaryButton : . cancel ( )
)
case . changeDatabaseKey :
return Alert (
title : Text ( " Change database passphrase? " ) ,
message : Text ( " Database encryption passphrase will be updated. \n " ) + storeSecurelyDanger ( ) ,
primaryButton : . destructive ( Text ( " Update " ) ) { encryptDatabase ( ) } ,
secondaryButton : . cancel ( )
)
case . databaseEncrypted :
return Alert ( title : Text ( " Database encrypted! " ) )
case . currentPassphraseError :
return Alert (
2022-09-14 14:04:41 +01:00
title : Text ( " Wrong passphrase! " ) ,
2022-09-08 17:36:16 +01:00
message : Text ( " Please enter correct current passphrase. " )
2022-09-07 12:49:41 +01:00
)
case let . error ( title , error ) :
2022-11-25 13:50:26 +00:00
return Alert ( title : Text ( title ) , message : Text ( error ) )
2022-09-07 12:49:41 +01:00
}
}
2024-03-11 21:17:28 +07:00
private func removePassphraseFromKeyChain ( ) {
if kcDatabasePassword . remove ( ) {
logger . debug ( " passphrase removed from keychain " )
setUseKeychain ( false )
storedKey = false
} else {
alert = . error ( title : " Keychain error " , error : " Failed to remove passphrase " )
}
}
2022-09-07 12:49:41 +01:00
private func storeSecurelySaved ( ) -> Text {
Text ( " Please store passphrase securely, you will NOT be able to change it if you lose it. " )
}
private func storeSecurelyDanger ( ) -> Text {
Text ( " Please store passphrase securely, you will NOT be able to access chat if you lose it. " )
}
private func operationEnded ( _ dbAlert : DatabaseEncryptionAlert ) async {
await MainActor . run {
m . chatDbChanged = true
2022-09-23 12:51:40 +01:00
m . chatInitialized = false
2022-09-07 12:49:41 +01:00
progressIndicator = false
alert = dbAlert
}
}
}
2023-03-22 15:58:01 +00:00
struct PassphraseField : View {
2024-07-03 22:42:13 +01:00
@ EnvironmentObject var theme : AppTheme
2022-09-07 12:49:41 +01:00
@ Binding var key : String
var placeholder : LocalizedStringKey
var valid : Bool
var showStrength = false
2022-09-08 17:36:16 +01:00
var onSubmit : ( ) -> Void = { }
2022-09-07 12:49:41 +01:00
@ State private var showKey = false
var body : some View {
ZStack ( alignment : . leading ) {
let iconColor = valid
2024-07-03 22:42:13 +01:00
? ( showStrength && key != " " ? PassphraseStrength ( passphrase : key ) . color : theme . colors . secondary )
2022-09-07 12:49:41 +01:00
: . red
Image ( systemName : valid ? ( showKey ? " eye.slash " : " eye " ) : " exclamationmark.circle " )
. resizable ( )
. scaledToFit ( )
. frame ( width : 20 , height : 22 , alignment : . center )
. foregroundColor ( iconColor )
. onTapGesture { showKey = ! showKey }
textField ( )
. disableAutocorrection ( true )
. autocapitalization ( . none )
. submitLabel ( . done )
. padding ( . leading , 36 )
2022-09-08 17:36:16 +01:00
. onSubmit ( onSubmit )
2022-09-07 12:49:41 +01:00
}
}
@ ViewBuilder func textField ( ) -> some View {
if showKey {
TextField ( placeholder , text : $ key )
} else {
SecureField ( placeholder , text : $ key )
}
}
}
// b a s e d o n h t t p s : / / g e n e r a t e p a s s w o r d s . o r g / h o w - t o - c a l c u l a t e - e n t r o p y /
private func passphraseEnthropy ( _ s : String ) -> Double {
var hasDigits = false
var hasUppercase = false
var hasLowercase = false
var hasSymbols = false
for c in s {
if c . isNumber {
hasDigits = true
} else if c . isLetter {
if c . isUppercase { hasUppercase = true }
else { hasLowercase = true }
} else if c . isASCII {
hasSymbols = true
}
}
let poolSize : Double = ( hasDigits ? 10 : 0 ) + ( hasUppercase ? 26 : 0 ) + ( hasLowercase ? 26 : 0 ) + ( hasSymbols ? 32 : 0 )
return Double ( s . count ) * log2 ( poolSize )
}
enum PassphraseStrength {
case veryWeak
case weak
case reasonable
case strong
init ( passphrase s : String ) {
let enthropy = passphraseEnthropy ( s )
2022-09-24 15:45:10 +03:00
self = enthropy > 100
2022-09-07 12:49:41 +01:00
? . strong
2022-09-24 15:45:10 +03:00
: enthropy > 70
2022-09-07 12:49:41 +01:00
? . reasonable
2022-09-24 15:45:10 +03:00
: enthropy > 40
2022-09-07 12:49:41 +01:00
? . weak
: . veryWeak
}
var color : Color {
switch self {
case . veryWeak : return . red
case . weak : return . orange
case . reasonable : return . yellow
case . strong : return . green
}
}
}
func validKey ( _ s : String ) -> Bool {
for c in s { if c . isWhitespace || ! c . isASCII { return false } }
return true
}
struct DatabaseEncryptionView_Previews : PreviewProvider {
static var previews : some View {
2024-11-30 23:29:27 +07:00
DatabaseEncryptionView ( useKeychain : Binding . constant ( true ) , migration : false , stopChatRunBlockStartChat : { _ , _ in true } )
2022-09-07 12:49:41 +01:00
}
}