mirror of
https://github.com/Eugeny/tabby.git
synced 2025-10-05 06:24:56 +00:00
allow storing private keys in the vault
This commit is contained in:
13
terminus-core/src/api/fileProvider.ts
Normal file
13
terminus-core/src/api/fileProvider.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export abstract class FileProvider {
|
||||
name: string
|
||||
|
||||
async isAvailable (): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
abstract selectAndStoreFile (description: string): Promise<string>
|
||||
abstract retrieveFile (key: string): Promise<Buffer>
|
||||
}
|
@@ -15,6 +15,7 @@ export { MenuItemOptions } from './menu'
|
||||
export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
||||
export { HostWindowService } from './hostWindow'
|
||||
export { HostAppService, Platform } from './hostApp'
|
||||
export { FileProvider } from './fileProvider'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService } from '../services/config.service'
|
||||
@@ -27,5 +28,6 @@ export { NotificationsService } from '../services/notifications.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { TabsService } from '../services/tabs.service'
|
||||
export { UpdaterService } from '../services/updater.service'
|
||||
export { VaultService, Vault, VaultSecret } from '../services/vault.service'
|
||||
export { VaultService, Vault, VaultSecret, VAULT_SECRET_TYPE_FILE } from '../services/vault.service'
|
||||
export { FileProvidersService } from '../services/fileProviders.service'
|
||||
export * from '../utils'
|
||||
|
@@ -27,10 +27,11 @@ import { AutofocusDirective } from './directives/autofocus.directive'
|
||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService } from './api'
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider } from './api'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { VaultFileProvider } from './services/vault.service'
|
||||
|
||||
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
@@ -53,6 +54,7 @@ const PROVIDERS = [
|
||||
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
|
||||
{ provide: CLIHandler, useClass: LastCLIHandler, multi: true },
|
||||
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } },
|
||||
{ provide: FileProvider, useClass: VaultFileProvider, multi: true },
|
||||
]
|
||||
|
||||
/** @hidden */
|
||||
|
48
terminus-core/src/services/fileProviders.service.ts
Normal file
48
terminus-core/src/services/fileProviders.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { AppService, FileProvider, NotificationsService } from '../api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FileProvidersService {
|
||||
/** @hidden */
|
||||
private constructor (
|
||||
private app: AppService,
|
||||
private notifications: NotificationsService,
|
||||
@Inject(FileProvider) private fileProviders: FileProvider[],
|
||||
) { }
|
||||
|
||||
async selectAndStoreFile (description: string): Promise<string> {
|
||||
const p = await this.selectProvider()
|
||||
return p.selectAndStoreFile(description)
|
||||
}
|
||||
|
||||
async retrieveFile (key: string): Promise<Buffer> {
|
||||
for (const p of this.fileProviders) {
|
||||
try {
|
||||
return await p.retrieveFile(key)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
throw new Error('Not found')
|
||||
}
|
||||
|
||||
async selectProvider (): Promise<FileProvider> {
|
||||
const providers: FileProvider[] = []
|
||||
await Promise.all(this.fileProviders.map(async p => {
|
||||
if (await p.isAvailable()) {
|
||||
providers.push(p)
|
||||
}
|
||||
}))
|
||||
if (!providers.length) {
|
||||
this.notifications.error('Vault master passphrase needs to be set to allow storing secrets')
|
||||
throw new Error('No available file providers')
|
||||
}
|
||||
if (providers.length === 1) {
|
||||
return providers[0]
|
||||
}
|
||||
return this.app.showSelector('Select file storage', providers.map(p => ({
|
||||
name: p.name,
|
||||
result: p,
|
||||
})))
|
||||
}
|
||||
}
|
@@ -6,6 +6,8 @@ import { AsyncSubject, Subject, Observable } from 'rxjs'
|
||||
import { wrapPromise } from '../utils'
|
||||
import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component'
|
||||
import { NotificationsService } from '../services/notifications.service'
|
||||
import { FileProvider } from '../api/fileProvider'
|
||||
import { PlatformService } from '../api/platform'
|
||||
|
||||
const PBKDF_ITERATIONS = 100000
|
||||
const PBKDF_DIGEST = 'sha512'
|
||||
@@ -80,6 +82,8 @@ async function decryptVault (vault: StoredVault, passphrase: string): Promise<Va
|
||||
return migrateVaultContent(JSON.parse(plaintext))
|
||||
}
|
||||
|
||||
export const VAULT_SECRET_TYPE_FILE = 'file'
|
||||
|
||||
// Don't make it accessible through VaultService fields
|
||||
let _rememberedPassphrase: string|null = null
|
||||
|
||||
@@ -161,7 +165,7 @@ export class VaultService {
|
||||
setTimeout(() => {
|
||||
_rememberedPassphrase = null
|
||||
// avoid multiple consequent prompts
|
||||
}, Math.min(1000, rememberFor * 60000))
|
||||
}, Math.max(1000, rememberFor * 60000))
|
||||
_rememberedPassphrase = passphrase
|
||||
}
|
||||
|
||||
@@ -212,3 +216,51 @@ export class VaultService {
|
||||
return !!this.store
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class VaultFileProvider extends FileProvider {
|
||||
name = 'Vault'
|
||||
prefix = 'vault://'
|
||||
|
||||
constructor (
|
||||
private vault: VaultService,
|
||||
private platform: PlatformService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async isAvailable (): Promise<boolean> {
|
||||
return this.vault.isEnabled()
|
||||
}
|
||||
|
||||
async selectAndStoreFile (description: string): Promise<string> {
|
||||
const transfers = await this.platform.startUpload()
|
||||
if (!transfers.length) {
|
||||
throw new Error('Nothing selected')
|
||||
}
|
||||
const transfer = transfers[0]
|
||||
const id = (await wrapPromise(this.zone, promisify(crypto.randomBytes)(32))).toString('hex')
|
||||
this.vault.addSecret({
|
||||
type: VAULT_SECRET_TYPE_FILE,
|
||||
key: {
|
||||
id,
|
||||
description,
|
||||
},
|
||||
value: (await transfer.readAll()).toString('base64'),
|
||||
})
|
||||
return `${this.prefix}${id}`
|
||||
}
|
||||
|
||||
async retrieveFile (key: string): Promise<Buffer> {
|
||||
if (!key.startsWith(this.prefix)) {
|
||||
throw new Error('Incorrect type')
|
||||
}
|
||||
const secret = await this.vault.getSecret(VAULT_SECRET_TYPE_FILE, { id: key.substring(this.prefix.length) })
|
||||
if (!secret) {
|
||||
throw new Error('Not found')
|
||||
}
|
||||
return Buffer.from(secret.value, 'base64')
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user