mirror of
https://github.com/Eugeny/tabby.git
synced 2025-10-04 22:14:55 +00:00
allow config encryption
This commit is contained in:
@@ -80,4 +80,5 @@ export abstract class PlatformService {
|
||||
abstract listFonts (): Promise<string[]>
|
||||
abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
|
||||
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
|
||||
abstract quit (): void
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ export class UnlockVaultModalComponent {
|
||||
) { }
|
||||
|
||||
ngOnInit (): void {
|
||||
this.rememberFor = window.localStorage.vaultRememberPassphraseFor ?? 0
|
||||
this.rememberFor = parseInt(window.localStorage.vaultRememberPassphraseFor ?? 0)
|
||||
setTimeout(() => {
|
||||
this.input.nativeElement.focus()
|
||||
})
|
||||
|
@@ -23,3 +23,4 @@ electronFlags:
|
||||
enableAutomaticUpdates: true
|
||||
version: 1
|
||||
vault: null
|
||||
encrypted: false
|
||||
|
@@ -4,6 +4,7 @@ import { Injectable, Inject } from '@angular/core'
|
||||
import { ConfigProvider } from '../api/configProvider'
|
||||
import { PlatformService } from '../api/platform'
|
||||
import { HostAppService } from './hostApp.service'
|
||||
import { Vault, VaultService } from './vault.service'
|
||||
const deepmerge = require('deepmerge')
|
||||
|
||||
const configMerge = (a, b) => deepmerge(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
@@ -105,10 +106,15 @@ export class ConfigService {
|
||||
private constructor (
|
||||
private hostApp: HostAppService,
|
||||
private platform: PlatformService,
|
||||
private vault: VaultService,
|
||||
@Inject(ConfigProvider) private configProviders: ConfigProvider[],
|
||||
) {
|
||||
this.defaults = this.mergeDefaults()
|
||||
this.init()
|
||||
setTimeout(() => this.init())
|
||||
vault.contentChanged$.subscribe(() => {
|
||||
this.store.vault = vault.store
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
mergeDefaults (): unknown {
|
||||
@@ -152,13 +158,16 @@ export class ConfigService {
|
||||
} else {
|
||||
this._store = { version: LATEST_VERSION }
|
||||
}
|
||||
this._store = await this.maybeDecryptConfig(this._store)
|
||||
this.migrate(this._store)
|
||||
this.store = new ConfigProxy(this._store, this.defaults)
|
||||
this.vault.setStore(this.store.vault)
|
||||
}
|
||||
|
||||
async save (): Promise<void> {
|
||||
// Scrub undefined values
|
||||
const cleanStore = JSON.parse(JSON.stringify(this._store))
|
||||
let cleanStore = JSON.parse(JSON.stringify(this._store))
|
||||
cleanStore = await this.maybeEncryptConfig(cleanStore)
|
||||
await this.platform.saveConfig(yaml.dump(cleanStore))
|
||||
this.emitChange()
|
||||
this.hostApp.broadcastConfigChange(JSON.parse(JSON.stringify(this.store)))
|
||||
@@ -207,7 +216,7 @@ export class ConfigService {
|
||||
return services.filter(service => {
|
||||
for (const pluginName in this.servicesCache) {
|
||||
if (this.servicesCache[pluginName].includes(service.constructor)) {
|
||||
return !this.store.pluginBlacklist.includes(pluginName)
|
||||
return !this.store?.pluginBlacklist?.includes(pluginName)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -227,6 +236,7 @@ export class ConfigService {
|
||||
|
||||
private emitChange (): void {
|
||||
this.changed.next()
|
||||
this.vault.setStore(this.store.vault)
|
||||
}
|
||||
|
||||
private migrate (config) {
|
||||
@@ -241,4 +251,67 @@ export class ConfigService {
|
||||
config.version = 1
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeDecryptConfig (store) {
|
||||
if (!store.encrypted) {
|
||||
return store
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations
|
||||
let decryptedVault: Vault
|
||||
while (true) {
|
||||
try {
|
||||
const passphrase = await this.vault.getPassphrase()
|
||||
decryptedVault = await this.vault.decrypt(store.vault, passphrase)
|
||||
break
|
||||
} catch (e) {
|
||||
let result = await this.platform.showMessageBox({
|
||||
type: 'error',
|
||||
message: 'Could not decrypt config',
|
||||
detail: e.toString(),
|
||||
buttons: ['Try again', 'Erase config', 'Quit'],
|
||||
defaultId: 0,
|
||||
})
|
||||
if (result.response === 2) {
|
||||
this.platform.quit()
|
||||
}
|
||||
if (result.response === 1) {
|
||||
result = await this.platform.showMessageBox({
|
||||
type: 'warning',
|
||||
message: 'Are you sure?',
|
||||
detail: e.toString(),
|
||||
buttons: ['Erase config', 'Quit'],
|
||||
defaultId: 1,
|
||||
})
|
||||
if (result.response === 1) {
|
||||
this.platform.quit()
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete decryptedVault.config.vault
|
||||
delete decryptedVault.config.encrypted
|
||||
return {
|
||||
...decryptedVault.config,
|
||||
vault: store.vault,
|
||||
encrypted: store.encrypted,
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeEncryptConfig (store) {
|
||||
if (!store.encrypted) {
|
||||
return store
|
||||
}
|
||||
const vault = await this.vault.load()
|
||||
if (!vault) {
|
||||
throw new Error('Vault not configured')
|
||||
}
|
||||
vault.config = { ...store }
|
||||
delete vault.config.vault
|
||||
delete vault.config.encrypted
|
||||
return {
|
||||
vault: await this.vault.encrypt(vault),
|
||||
encrypted: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ export class ThemesService {
|
||||
private config: ConfigService,
|
||||
@Inject(Theme) private themes: Theme[],
|
||||
) {
|
||||
this.applyTheme(this.findTheme('Standard')!)
|
||||
config.ready$.toPromise().then(() => {
|
||||
this.applyCurrentTheme()
|
||||
config.changed$.subscribe(() => {
|
||||
@@ -38,7 +39,7 @@ export class ThemesService {
|
||||
document.querySelector('head')!.appendChild(this.styleElement)
|
||||
}
|
||||
this.styleElement.textContent = theme.css
|
||||
document.querySelector('style#custom-css')!.innerHTML = this.config.store.appearance.css
|
||||
document.querySelector('style#custom-css')!.innerHTML = this.config.store?.appearance?.css
|
||||
this.themeChanged.next(theme)
|
||||
}
|
||||
|
||||
|
@@ -2,8 +2,7 @@ import * as crypto from 'crypto'
|
||||
import { promisify } from 'util'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { AsyncSubject, Observable } from 'rxjs'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { AsyncSubject, Subject, Observable } from 'rxjs'
|
||||
import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component'
|
||||
import { NotificationsService } from '../services/notifications.service'
|
||||
|
||||
@@ -28,11 +27,13 @@ export interface VaultSecret {
|
||||
}
|
||||
|
||||
export interface Vault {
|
||||
config: any
|
||||
secrets: VaultSecret[]
|
||||
}
|
||||
|
||||
function migrateVaultContent (content: any): Vault {
|
||||
return {
|
||||
config: content.config,
|
||||
secrets: content.secrets ?? [],
|
||||
}
|
||||
}
|
||||
@@ -86,34 +87,27 @@ export class VaultService {
|
||||
/** Fires once when the config is loaded */
|
||||
get ready$ (): Observable<boolean> { return this.ready }
|
||||
|
||||
enabled = false
|
||||
get contentChanged$ (): Observable<void> { return this.contentChanged }
|
||||
|
||||
store: StoredVault|null = null
|
||||
private ready = new AsyncSubject<boolean>()
|
||||
private contentChanged = new Subject<void>()
|
||||
|
||||
/** @hidden */
|
||||
private constructor (
|
||||
private config: ConfigService,
|
||||
private zone: NgZone,
|
||||
private notifications: NotificationsService,
|
||||
private ngbModal: NgbModal,
|
||||
) {
|
||||
config.ready$.toPromise().then(() => {
|
||||
this.onConfigChange()
|
||||
this.ready.next(true)
|
||||
this.ready.complete()
|
||||
config.changed$.subscribe(() => {
|
||||
this.onConfigChange()
|
||||
})
|
||||
})
|
||||
}
|
||||
) { }
|
||||
|
||||
async setEnabled (enabled: boolean, passphrase?: string): Promise<void> {
|
||||
if (enabled) {
|
||||
if (!this.config.store.vault) {
|
||||
if (!this.store) {
|
||||
await this.save(migrateVaultContent({}), passphrase)
|
||||
}
|
||||
} else {
|
||||
this.config.store.vault = null
|
||||
await this.config.save()
|
||||
this.store = null
|
||||
this.contentChanged.next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,15 +115,12 @@ export class VaultService {
|
||||
return !!_rememberedPassphrase
|
||||
}
|
||||
|
||||
async load (passphrase?: string): Promise<Vault|null> {
|
||||
if (!this.config.store.vault) {
|
||||
return null
|
||||
}
|
||||
async decrypt (storage: StoredVault, passphrase?: string): Promise<Vault> {
|
||||
if (!passphrase) {
|
||||
passphrase = await this.getPassphrase()
|
||||
}
|
||||
try {
|
||||
return await this.wrapPromise(decryptVault(this.config.store.vault, passphrase))
|
||||
return await this.wrapPromise(decryptVault(storage, passphrase))
|
||||
} catch (e) {
|
||||
_rememberedPassphrase = null
|
||||
if (e.toString().includes('BAD_DECRYPT')) {
|
||||
@@ -139,15 +130,27 @@ export class VaultService {
|
||||
}
|
||||
}
|
||||
|
||||
async save (vault: Vault, passphrase?: string): Promise<void> {
|
||||
async load (passphrase?: string): Promise<Vault|null> {
|
||||
if (!this.store) {
|
||||
return null
|
||||
}
|
||||
return this.decrypt(this.store, passphrase)
|
||||
}
|
||||
|
||||
async encrypt (vault: Vault, passphrase?: string): Promise<StoredVault|null> {
|
||||
if (!passphrase) {
|
||||
passphrase = await this.getPassphrase()
|
||||
}
|
||||
if (_rememberedPassphrase) {
|
||||
_rememberedPassphrase = passphrase
|
||||
}
|
||||
this.config.store.vault = await this.wrapPromise(encryptVault(vault, passphrase))
|
||||
await this.config.save()
|
||||
return this.wrapPromise(encryptVault(vault, passphrase))
|
||||
}
|
||||
|
||||
async save (vault: Vault, passphrase?: string): Promise<void> {
|
||||
await this.ready$.toPromise()
|
||||
this.store = await this.encrypt(vault, passphrase)
|
||||
this.contentChanged.next()
|
||||
}
|
||||
|
||||
async getPassphrase (): Promise<string> {
|
||||
@@ -156,7 +159,8 @@ export class VaultService {
|
||||
const { passphrase, rememberFor } = await modal.result
|
||||
setTimeout(() => {
|
||||
_rememberedPassphrase = null
|
||||
}, rememberFor * 60000)
|
||||
// avoid multiple consequent prompts
|
||||
}, Math.min(1000, rememberFor * 60000))
|
||||
_rememberedPassphrase = passphrase
|
||||
}
|
||||
|
||||
@@ -164,6 +168,7 @@ export class VaultService {
|
||||
}
|
||||
|
||||
async getSecret (type: string, key: Record<string, any>): Promise<VaultSecret|null> {
|
||||
await this.ready$.toPromise()
|
||||
const vault = await this.load()
|
||||
if (!vault) {
|
||||
return null
|
||||
@@ -172,6 +177,7 @@ export class VaultService {
|
||||
}
|
||||
|
||||
async addSecret (secret: VaultSecret): Promise<void> {
|
||||
await this.ready$.toPromise()
|
||||
const vault = await this.load()
|
||||
if (!vault) {
|
||||
return
|
||||
@@ -182,6 +188,7 @@ export class VaultService {
|
||||
}
|
||||
|
||||
async removeSecret (type: string, key: Record<string, any>): Promise<void> {
|
||||
await this.ready$.toPromise()
|
||||
const vault = await this.load()
|
||||
if (!vault) {
|
||||
return
|
||||
@@ -194,8 +201,14 @@ export class VaultService {
|
||||
return Object.keys(key).every(k => secret.key[k] === key[k])
|
||||
}
|
||||
|
||||
private onConfigChange () {
|
||||
this.enabled = !!this.config.store.vault
|
||||
setStore (store: StoredVault): void {
|
||||
this.store = store
|
||||
this.ready.next(true)
|
||||
this.ready.complete()
|
||||
}
|
||||
|
||||
isEnabled (): boolean {
|
||||
return !!this.store
|
||||
}
|
||||
|
||||
private wrapPromise <T> (promise: Promise<T>): Promise<T> {
|
||||
|
Reference in New Issue
Block a user