import * as kdbxweb from 'kdbxweb'; import { SettingsStore } from 'comp/settings/settings-store'; import { Links } from 'const/links'; import { SignatureVerifier } from 'util/data/signature-verifier'; import { Logger } from 'util/logger'; import { PluginGalleryData } from 'plugins/types'; import { Model } from 'util/model'; class PluginGallery extends Model { logger = new Logger('plugin-gallery'); gallery?: PluginGalleryData; loading = false; loadError = false; async loadPlugins(): Promise { if (this.gallery) { return Promise.resolve(this.gallery); } this.loading = true; this.loadError = false; const ts = this.logger.ts(); try { const data = await new Promise((resolve, reject) => { this.logger.info('Loading plugins...'); const xhr = new XMLHttpRequest(); xhr.open('GET', Links.Plugins + '/plugins.json'); xhr.responseType = 'json'; xhr.send(); xhr.addEventListener('load', () => { const resp = xhr.response as PluginGalleryData; if (!resp?.signature) { return reject('Empty plugin gallery data'); } resolve(resp); }); xhr.addEventListener('error', () => { reject('Network error during loading plugins'); }); }); this.loading = false; await this.verifySignature(data); this.loadError = false; this.logger.info(`Loaded ${data.plugins.length} plugins`, this.logger.ts(ts)); this.gallery = data; await this.saveGallery(data); return this.gallery; } catch (e) { this.loading = false; this.loadError = true; this.logger.error('Error loading plugin gallery', e); throw e; } } private async verifySignature(gallery: PluginGalleryData): Promise { const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, ''); const valid = await SignatureVerifier.verify( kdbxweb.ByteUtils.stringToBytes(dataToVerify), gallery.signature ); if (!valid) { this.logger.error('JSON signature invalid'); throw new Error('JSON signature invalid'); } } async getCachedGallery(): Promise { const ts = this.logger.ts(); const data = (await SettingsStore.load('plugin-gallery')) as PluginGalleryData; if (data) { try { await this.verifySignature(data); this.logger.info('Loaded cached plugin gallery', this.logger.ts(ts)); return data; } catch (err) { this.logger.error('Cannot load cached plugin gallery: signature validation error'); } } } private saveGallery(data: PluginGalleryData): Promise { return SettingsStore.save('plugin-gallery', data); } } const instance = new PluginGallery(); export { instance as PluginGallery };