allow storing private keys in the vault

This commit is contained in:
Eugene Pankov
2021-06-19 21:54:27 +02:00
parent e41fe9f4c0
commit 51f62c9719
17 changed files with 567 additions and 44 deletions

View 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>
}

View File

@@ -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'

View File

@@ -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 */

View 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,
})))
}
}

View File

@@ -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')
}
}