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
2022-09-08 17:36:16 +01:00
@ Binding var useKeychain : 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 ( )
@ State private var storedKey = getDatabaseKey ( ) != nil
@ State private var currentKey = " "
@ State private var newKey = " "
@ State private var confirmNewKey = " "
@ State private var currentKeyShown = false
var body : some View {
ZStack {
databaseEncryptionView ( )
if progressIndicator {
ProgressView ( ) . scaleEffect ( 2 )
}
}
}
private func databaseEncryptionView ( ) -> some View {
List {
Section {
2022-09-08 17:36:16 +01:00
settingsRow ( storedKey ? " key.fill " : " key " , color : storedKey ? . green : . secondary ) {
2022-09-07 12:49:41 +01:00
Toggle ( " Save passphrase in Keychain " , isOn : $ useKeychainToggle )
. onChange ( of : useKeychainToggle ) { _ in
if useKeychainToggle {
setUseKeychain ( true )
} else if storedKey {
alert = . keychainRemoveKey
} else {
setUseKeychain ( false )
}
}
. disabled ( initialRandomDBPassphrase )
}
if ! initialRandomDBPassphrase && m . chatDbEncrypted = = true {
DatabaseKeyField ( key : $ currentKey , placeholder : " Current passphrase… " , valid : validKey ( currentKey ) )
}
DatabaseKeyField ( key : $ newKey , placeholder : " New passphrase… " , valid : validKey ( newKey ) , showStrength : true )
DatabaseKeyField ( key : $ confirmNewKey , placeholder : " Confirm new passphrase… " , valid : confirmNewKey = = " " || newKey = = confirmNewKey )
settingsRow ( " lock.rotation " ) {
Button ( " Update database passphrase " ) {
alert = currentKey = = " "
? ( useKeychain ? . encryptDatabaseSaved : . encryptDatabase )
: ( useKeychain ? . changeDatabaseKeySaved : . changeDatabaseKey )
}
}
. disabled (
2022-09-25 20:53:32 +01:00
( m . chatDbEncrypted = = true && currentKey = = " " ) ||
2022-09-07 12:49:41 +01:00
currentKey = = newKey ||
newKey != confirmNewKey ||
newKey = = " " ||
! validKey ( currentKey ) ||
! validKey ( newKey )
)
} header : {
Text ( " " )
} 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 {
Text ( " Database is encrypted using a random passphrase, you can change it. " )
} else {
Text ( " **Please note**: you will NOT be able to recover or change passphrase if you lose it. " )
}
} else {
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 {
Text ( " **Warning**: Instant push notifications require passphrase saved in Keychain. " )
}
}
}
. padding ( . top , 1 )
. font ( . callout )
}
}
. onAppear {
if initialRandomDBPassphrase { currentKey = getDatabaseKey ( ) ? ? " " }
}
. disabled ( m . chatRunning != false )
. alert ( item : $ alert ) { item in databaseEncryptionAlert ( item ) }
}
private func encryptDatabase ( ) {
progressIndicator = true
Task {
do {
2022-09-17 16:41:20 +04:00
encryptionStartedDefault . set ( true )
encryptionStartedAtDefault . set ( Date . now )
2022-09-07 12:49:41 +01:00
try await apiStorageEncryption ( currentKey : currentKey , newKey : newKey )
2022-09-17 16:41:20 +04:00
encryptionStartedDefault . set ( false )
2022-09-07 12:49:41 +01:00
initialRandomDBPassphraseGroupDefault . set ( false )
if useKeychain {
if setDatabaseKey ( newKey ) {
await resetFormAfterEncryption ( true )
await operationEnded ( . databaseEncrypted )
} else {
await resetFormAfterEncryption ( )
await operationEnded ( . error ( title : " Keychain error " , error : " Error saving passphrase to keychain " ) )
}
} else {
await resetFormAfterEncryption ( )
await operationEnded ( . databaseEncrypted )
}
} catch let error {
2023-01-19 16:22:56 +00:00
if case . chatCmdError ( _ , . errorDatabase ( . errorExport ( . errorNotADatabase ) ) ) = error as ? ChatResponse {
2022-09-07 12:49:41 +01:00
await operationEnded ( . currentPassphraseError )
} else {
2022-11-25 13:50:26 +00:00
await operationEnded ( . error ( title : " Error encrypting database " , error : " \( responseError ( error ) ) " ) )
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
storeDBPassphraseGroupDefault . set ( value )
}
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 " ) ) {
if removeDatabaseKey ( ) {
2022-09-25 20:53:32 +01:00
logger . debug ( " passphrase removed from keychain " )
2022-09-07 12:49:41 +01:00
setUseKeychain ( false )
storedKey = false
} else {
alert = . error ( title : " Keychain error " , error : " Failed to remove passphrase " )
}
} ,
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
}
}
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
}
}
}
struct DatabaseKeyField : View {
@ 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
? ( showStrength && key != " " ? PassphraseStrength ( passphrase : key ) . color : . secondary )
: . 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 {
2022-09-08 17:36:16 +01:00
DatabaseEncryptionView ( useKeychain : Binding . constant ( true ) )
2022-09-07 12:49:41 +01:00
}
}