Skip to content

Commit b38261b

Browse files
Handle macOS keychain access failures explicitly
1 parent db314d0 commit b38261b

File tree

9 files changed

+101
-13
lines changed

9 files changed

+101
-13
lines changed

src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export default {
5656
trapFocus: true
5757
})
5858
}
59+
}).catch(error => {
60+
this.$authManager.showKeychainAccessError(error, 'Keychain Access Needed')
5961
})
6062
6163
ipcRenderer.on('open-settings', () => {

src/authentication/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ class AuthManager {
2424
this.$buefy = vue.$buefy
2525
}
2626

27+
public showKeychainAccessError (error: any, title: string = Keytar.accessErrorTitle): void {
28+
const details = error && error.originalMessage ? `<br/><br/><small>${error.originalMessage}</small>` : ''
29+
30+
this.$buefy.dialog.alert({
31+
title,
32+
message: `${Keytar.accessErrorMessage}${details}`,
33+
type: 'is-danger',
34+
hasIcon: true,
35+
ariaRole: 'alertdialog',
36+
ariaModal: true
37+
})
38+
}
39+
2740
public async authenticate (pin: string): Promise<boolean> {
2841
this.authenticated = await this.validateAuthentication(pin)
2942

@@ -134,7 +147,7 @@ class AuthManager {
134147
}
135148

136149
public async changePin (pin: string): Promise<boolean> {
137-
Keytar.setCredentials(Keytar.appService, 'application', pin)
150+
await Keytar.setCredentials(Keytar.appService, 'application', pin)
138151

139152
this.authenticated = true
140153

src/utils/keytar/index.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,42 @@ import Log from 'electron-log'
44
export default class Keytar {
55
static readonly appService: string = 'MyVergies'
66
static readonly walletService: string = 'MyVergies Wallet'
7+
static readonly accessErrorTitle: string = 'Keychain Access Needed'
8+
static readonly accessErrorMessage: string = 'MyVergies could not access the macOS Keychain. Enter your Mac login password and choose "Always Allow" if prompted, then try again. If you denied access earlier, restart MyVergies and retry.'
79

8-
static setCredentials (service: string, account: string, credentials: string) {
10+
static async setCredentials (service: string, account: string, credentials: string) {
911
Log.info(`set credentials for service "${service}" and account "${account}"`)
1012

11-
return electron.ipcRenderer.sendSync('set-password', service, account, credentials)
13+
return await this.request('set-password', service, account, credentials)
1214
}
1315

14-
static getCredentials (service: string, account: string) {
16+
static async getCredentials (service: string, account: string) {
1517
Log.info(`get credentials for service "${service}" and account "${account}"`)
1618

17-
return electron.ipcRenderer.sendSync('get-password', service, account)
19+
return await this.request('get-password', service, account)
1820
}
1921

20-
static deleteCredentials (service: string, account: string) {
22+
static async deleteCredentials (service: string, account: string) {
2123
Log.info(`delete credentials for service "${service}" and account "${account}"`)
2224

23-
return electron.ipcRenderer.sendSync('delete-password', service, account)
25+
return await this.request('delete-password', service, account)
26+
}
27+
28+
static isAccessError (error: any): boolean {
29+
return Boolean(error && error.isKeytarAccessError)
30+
}
31+
32+
protected static async request (channel: string, ...args: any[]) {
33+
try {
34+
return await electron.ipcRenderer.invoke(channel, ...args)
35+
} catch (error) {
36+
Log.error(`keychain request "${channel}" failed: ${error instanceof Error ? error.message : String(error)}`)
37+
38+
const wrappedError: any = new Error(Keytar.accessErrorMessage)
39+
wrappedError.isKeytarAccessError = true
40+
wrappedError.originalMessage = error instanceof Error ? error.message : String(error)
41+
42+
throw wrappedError
43+
}
2444
}
2545
}

src/utils/keytar/main.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,39 @@ import keytar from 'keytar'
22
import electron from 'electron'
33
import Log from 'electron-log'
44

5-
electron.ipcMain.on('get-password', async (event, service: string, wallet: string) => {
5+
const logKeytarError = (action: string, service: string, wallet: string, error: any) => {
6+
Log.error(`keychain ${action} failed for service "${service}" and wallet "${wallet}": ${error instanceof Error ? error.message : String(error)}`)
7+
}
8+
9+
electron.ipcMain.handle('get-password', async (event, service: string, wallet: string) => {
610
Log.info(`request password for service "${service}" and wallet "${wallet}"`)
711

8-
event.returnValue = await keytar.getPassword(service, wallet)
12+
try {
13+
return await keytar.getPassword(service, wallet)
14+
} catch (error) {
15+
logKeytarError('read', service, wallet, error)
16+
throw error
17+
}
918
})
1019

11-
electron.ipcMain.on('set-password', async (event, service: string, wallet: string, credentials: string) => {
20+
electron.ipcMain.handle('set-password', async (event, service: string, wallet: string, credentials: string) => {
1221
Log.info(`request set password for service "${service}" and wallet "${wallet}"`)
1322

14-
event.returnValue = await keytar.setPassword(service, wallet, credentials)
23+
try {
24+
return await keytar.setPassword(service, wallet, credentials)
25+
} catch (error) {
26+
logKeytarError('write', service, wallet, error)
27+
throw error
28+
}
1529
})
1630

17-
electron.ipcMain.on('delete-password', async (event, service: string, wallet: string) => {
31+
electron.ipcMain.handle('delete-password', async (event, service: string, wallet: string) => {
1832
Log.info(`request delete password for service "${service}" and wallet "${wallet}"`)
1933

20-
event.returnValue = await keytar.deletePassword(service, wallet)
34+
try {
35+
return await keytar.deletePassword(service, wallet)
36+
} catch (error) {
37+
logKeytarError('delete', service, wallet, error)
38+
throw error
39+
}
2140
})

src/views/Authentication/AuthenticationModal.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export default {
7979
} else {
8080
this.$emit('authenticated')
8181
}
82+
}).catch(error => {
83+
this.$authManager.showKeychainAccessError(error, 'Could Not Unlock Wallet')
8284
})
8385
},
8486

src/views/Settings/AddPinModal.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export default {
9898
message: this.$i18n.t('settings.pinAdded'),
9999
type: 'is-success'
100100
})
101+
}).catch(error => {
102+
this.$authManager.showKeychainAccessError(error, 'Could Not Save PIN')
101103
})
102104
},
103105

src/views/Settings/ChangePinModal.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export default {
125125
message: this.$i18n.t('settings.pinChanged'),
126126
type: 'is-success'
127127
})
128+
}).catch(error => {
129+
this.$authManager.showKeychainAccessError(error, 'Could Not Change PIN')
128130
})
129131
},
130132
@@ -135,6 +137,8 @@ export default {
135137
} else {
136138
this.stage = 1
137139
}
140+
}).catch(error => {
141+
this.$authManager.showKeychainAccessError(error, 'Could Not Read PIN')
138142
})
139143
},
140144

src/views/Wallet/Create/WalletSetupView.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ export default {
9090
9191
this.createdWallet = wallet
9292
}).catch(error => {
93+
if (this.$authManager && this.$authManager.showKeychainAccessError && error && error.isKeytarAccessError) {
94+
this.$authManager.showKeychainAccessError(error, 'Could Not Save Wallet')
95+
96+
return
97+
}
98+
9399
this.$buefy.dialog.alert({
94100
message: error.toString(),
95101
onConfirm: () => {

src/walletManager/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ const walletManager: PluginFunction<any> = function (vue: typeof Vue, options: a
99

1010
loadWallets(options.store).then((wallets: WalletConfigItem[]) => {
1111
vue.prototype.$walletManager.boot(new ManagerConfig(wallets))
12+
}).catch((error: any) => {
13+
if (Keytar.isAccessError(error)) {
14+
const details = error && error.originalMessage ? `<br/><br/><small>${error.originalMessage}</small>` : ''
15+
16+
vue.prototype.$buefy.dialog.alert({
17+
title: 'Could Not Load Wallets',
18+
message: `${Keytar.accessErrorMessage}${details}`,
19+
type: 'is-danger',
20+
hasIcon: true
21+
})
22+
23+
return
24+
}
25+
26+
vue.prototype.$buefy.dialog.alert({
27+
title: 'Could Not Load Wallets',
28+
message: error.toString(),
29+
type: 'is-danger',
30+
hasIcon: true
31+
})
1232
})
1333
}
1434

0 commit comments

Comments
 (0)