Sichere Browsererweiterungen entwickeln

Sichere Browsererweiterungen entwickeln

Im Gegensatz zur verbreiteten „Client-Server“-Architektur zeichnet sich die dezentrale Anwendungsarchitektur durch Folgendes aus:

  • Die Notwendigkeit, eine Datenbank mit Benutzeranmeldeinformationen wie Benutzernamen und Passwörtern zu speichern, entfällt. Zugangsinfos werden ausschließlich bei den Nutzern selbst gespeichert, und ihre Authentizität wird auf Protokollebene überprüft.
  • Es besteht keine Notwendigkeit für einen Server. Die Logik der Anwendung kann im Blockchain-Netzwerk ausgeführt werden, wo auch die erforderliche Datenmenge gespeichert werden kann.

Es gibt zwei relativ sichere Speicher für Benutzerschlüssel – Hardware-Wallets und Browser-Erweiterungen. Hardware-Wallets sind im Allgemeinen sehr sicher, jedoch oft schwierig zu bedienen und nicht kostenlos, während Browser-Erweiterungen eine ideale Kombination aus Sicherheit und Benutzerfreundlichkeit bieten können und für Endbenutzer vollständig kostenlos sein können.

Angesichts all dessen haben wir ein höchst sicheres Erweiterungsmodul entwickelt, das die Entwicklung dezentraler Anwendungen erleichtert und eine einfache API für Transaktionen und Signaturen bereitstellt.
Über diese Erfahrung werden wir Ihnen im Folgenden berichten.

In diesem Artikel finden Sie eine Schritt-für-Schritt-Anleitung, wie Sie eine Browsererweiterung schreiben, einschließlich Codebeispielen und Screenshots. Den gesamten Code finden Sie in Repository. Jeder Commit entspricht logisch einem Abschnitt dieses Artikels.

Eine kurze Geschichte der Browsererweiterungen

Browsererweiterungen gibt es schon seit einiger Zeit. Im Internet Explorer erschienen sie bereits 1999, im Firefox 2004. Lange Zeit gab es jedoch keinen einheitlichen Standard für Erweiterungen.

Man kann sagen, dass dieser mit den Erweiterungen in der vierten Version von Google Chrome eingeführt wurde. Natürlich gab es damals noch keine Spezifikation, aber die Chrome-API wurde zu deren Grundlage: Mit dem Erwerb eines erheblichen Marktanteils und dem integrierten App-Store setzte Chrome tatsächlich den Standard für Browsererweiterungen.

Mozilla hatte seinen eigenen Standard, erkannte jedoch die Popularität der Erweiterungen für Chrome und beschloss, eine kompatible API zu entwickeln. 2015 wurde auf Initiative von Mozilla eine spezielle Gruppe im Rahmen des World Wide Web Consortium (W3C) gegründet, um an den Spezifikationen für plattformübergreifende Erweiterungen zu arbeiten.

Als Grundlage wurde die bereits existierende API für Chrome-Erweiterungen verwendet. Die Arbeiten wurden mit Unterstützung von Microsoft durchgeführt (Google hat sich geweigert, an der Entwicklung des Standards teilzunehmen), und das Ergebnis war ein Entwurf. Spezifikationen.

Formell wird die Spezifikation von Edge, Firefox und Opera unterstützt (beachten Sie, dass Chrome in dieser Liste fehlt). In Wirklichkeit ist der Standard jedoch weitgehend mit Chrome kompatibel, da er faktisch auf seinen Erweiterungen basiert. Weitere Informationen zur WebExtensions API finden Sie hier. hier.

Struktur der Erweiterung

Die einzige Datei, die unbedingt für die Erweiterung benötigt wird, ist das Manifest (manifest.json). Es fungiert auch als „Eingangspunkt“ zur Erweiterung.

Manifest

Laut der Spezifikation ist die Manifestdatei eine gültige JSON-Datei. Eine vollständige Beschreibung der Schlüssel des Manifests mit Informationen darüber, welche Schlüssel in welchem Browser unterstützt werden, finden Sie hier. hier.

Schlüssel, die nicht in der Spezifikation enthalten sind, „könnten“ ignoriert werden (sowohl Chrome als auch Firefox melden Fehler, doch die Erweiterungen funktionieren weiterhin).

Ich möchte auf einige Punkte hinweisen.

  1. background — ein Objekt, das die folgenden Felder umfasst:
    1. scripts — ein Array von Skripten, die im Hintergrundkontext ausgeführt werden (darüber sprechen wir später).
    2. Seiten — Anstelle von Skripten, die auf einer leeren Seite ausgeführt werden, kann HTML mit Inhalten angegeben werden. In diesem Fall wird das Feld script ignoriert, und die Skripte müssen in die Seite mit den Inhalten eingefügt werden;
    3. anhaltende — ein binärer Flag, wenn nicht angegeben, wird der Browser den Hintergrundprozess "beenden", wenn er der Meinung ist, dass dieser nichts tut, und bei Bedarf neu starten. Andernfalls wird die Seite nur beim Schließen des Browsers entladen. Wird nicht in Firefox unterstützt.
  2. content_scripts — ein Array von Objekten, das das Laden verschiedener Skripte für verschiedene Webseiten ermöglicht. Jedes Objekt enthält folgende wichtige Felder:
    1. matchesURL-Muster, das bestimmt, ob ein bestimmtes Content-Skript aktiviert wird oder nicht.
    2. js — eine Liste von Skripten, die in diesem Match geladen werden;
    3. exclude_matches — schließt aus dem Feld Match URLs aus, die diesem Feld entsprechen.
  3. page_action — ist eigentlich ein Objekt, das für das Symbol verantwortlich ist, das neben der Adressleiste im Browser angezeigt wird, sowie für die Interaktion mit diesem. Erlaubt auch das Anzeigen eines Popup-Fensters, das mit eigenen HTML-, CSS- und JS-Dateien festgelegt wird.
    1. default_popup — Pfad zur HTML-Datei mit der Popup-Oberfläche, kann CSS und JS enthalten.
  4. Berechtigungen — ein Array zum Verwalten von Erweiterungsrechten. Es gibt 3 Arten von Rechten, die im Detail beschrieben sind hier
  5. web_accessible_resources — Ressourcen der Erweiterung, die von einer Webseite angefordert werden können, z. B. Bilder, JS-, CSS- und HTML-Dateien.
  6. externally_connectable — hier können die IDs anderer Erweiterungen und die Domänen von Webseiten, von denen eine Verbindung hergestellt werden kann, ausdrücklich angegeben werden. Die Domäne kann eine zweite Stufe oder höher sein. Funktioniert nicht in Firefox.

Ausführungskontext

Die Erweiterung hat drei Ausführungskontexte, das bedeutet, die Anwendung besteht aus drei Teilen mit unterschiedlichem Zugriffsniveau auf die Browser-API.

Erweiterungskontext

Hier steht der Großteil der API zur Verfügung. In diesem Kontext „leben“:

  1. Hintergrundseite — die „Backend“-Komponente der Erweiterung. Die Datei wird im Manifest unter dem Schlüssel „background“ angegeben.
  2. Popup-Seite — die Popup-Seite, die erscheint, wenn auf das Symbol der Erweiterung geklickt wird. Im Manifest browser_action -> default_popup.
  3. Benutzerdefinierte Seite — die Erweiterungsseite, die in einem separaten Tab lebt, wie folgt chrome-extension:///customPage.html.

Dieser Kontext existiert unabhängig von Fenstern und Tabs des Browsers. Hintergrundseite ist einzigartig und läuft immer (außer auf der Event-Seite, wenn das Hintergrundskript bei einem Ereignis gestartet wird und nach dessen Ausführung "stirbt"). Popup-Seite ist aktiv, wenn das Popup-Fenster geöffnet ist, und Benutzerdefinierte Seite — solange der Tab mit ihm geöffnet ist. Es gibt keinen Zugriff auf andere Tabs und deren Inhalte aus diesem Kontext.

Inhaltsskript-Kontext

Die Datei des Inhaltsskripts wird mit jedem Browser-Tab gestartet. Es hat Zugriff auf einen Teil der API der Erweiterung und auf den DOM-Baum der Webseite. Genau diese Inhaltsskripts sind für die Interaktion mit der Seite verantwortlich. Erweiterungen, die den DOM-Baum manipulieren, tun dies in den Inhaltsskripts – beispielsweise Werbeblocker oder Übersetzer. Außerdem kann das Inhaltsskript über den Standard postMessage.

Webseiten-Kontext

Das ist die Webseite selbst. Sie hat nichts mit der Erweiterung zu tun und hat keinen Zugriff darauf, es sei denn, die Domain dieser Seite ist ausdrücklich im Manifest angegeben (darüber – später).

Nachrichtenübertragung

Verschiedene Teile der Anwendung müssen Nachrichten untereinander austauschen. Dafür gibt es die API runtime.sendMessage um eine Nachricht zu senden background und tabs.sendMessage zum Senden von Nachrichten an die Seite (Inhalts-Skript, Popup oder Webseite, falls vorhanden externally_connectable). Nachfolgend ein Beispiel für den Zugriff auf die Chrome-API.

// Сообщением может быть любой JSON сериализуемый объект
const msg = {a: 'foo', b: 'bar'};

// extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта)
chrome.runtime.sendMessage(extensionId, msg);

// Так выглядит обработчик
chrome.runtime.onMessage.addListener((msg) => console.log(msg))

// Можно слать сообщения вкладкам зная их id
chrome.tabs.sendMessage(tabId, msg)

// Получить к вкладкам и их id можно, например, вот так
chrome.tabs.query(
    {currentWindow: true, active : true},
    function(tabArray){
      tabArray.forEach(tab => console.log(tab.id))
    }
)

Für eine umfassende Kommunikation können Verbindungen über runtime.connecthergestellt werden. Als Antwort erhalten wir runtime.Port, an den beliebig viele Nachrichten gesendet werden können, solange er geöffnet ist. Auf der Client-Seite, zum Beispiel contentscript, sieht das so aus:

// Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
    if (msg.question === "Who's there?")
        port.postMessage({answer: "Madame"});
    else if (msg.question === "Madame who?")
        port.postMessage({answer: "Madame... Bovary"});

Server oder Hintergrund:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения
chrome.runtime.onConnect.addListener(function(port) {
    console.assert(port.name === "knockknock");
    port.onMessage.addListener(function(msg) {
        if (msg.joke === "Knock knock")
            port.postMessage({question: "Who's there?"});
        else if (msg.answer === "Madame")
            port.postMessage({question: "Madame who?"});
        else if (msg.answer === "Madame... Bovary")
            port.postMessage({question: "I don't get it."});
    });
});

// Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте
chrome.runtime.onConnectExternal.addListener(function(port) {
    ...
});

Es gibt auch das Ereignis onDisconnect und die Methode trennen.

Anwendungsschema

Lass uns eine Browsererweiterung erstellen, die private Schlüssel speichert, Zugriff auf öffentliche Informationen bietet (Adresse, öffentlicher Schlüssel kommuniziert mit der Seite und ermöglicht es Drittanwendungen, Unterschriften für Transaktionen anzufordern.

Anwendungsentwicklung

Unsere Anwendung muss sowohl mit dem Benutzer interagieren als auch der Seite eine API für den Aufruf von Methoden (zum Beispiel zum Signieren von Transaktionen) bereitstellen. Es reicht nicht aus, nur contentscript zu verwenden, da es nur Zugriff auf das DOM, nicht jedoch auf den JS-Code der Seite hat. Eine Verbindung über runtime.connect können wir nicht herstellen, da die API auf allen Domains benötigt wird, im Manifest jedoch nur spezifische angegeben werden können. Daraus ergibt sich folgendes Schema:

Sichere Browsererweiterungen entwickeln

Es wird noch ein weiteres Skript geben — inpage, der in die Seite integriert wird. Er wird im Kontext der Seite ausgeführt und bietet eine API zur Interaktion mit der Erweiterung.

Anfang

Der gesamte Code der Browsererweiterung ist verfügbar unter GitHub. Im Verlauf der Beschreibung werden Links zu den Commits bereitgestellt.

Lassen Sie uns mit dem Manifest beginnen:

{
  // Name und Beschreibung, Version. Alles wird in chrome://extensions/?id= sichtbar sein
  "name": "Signer",
  "description": "Erweiterungs-Demo",
  "version": "0.0.1",
  "manifest_version": 2,

  // Skripte, die im Hintergrund ausgeführt werden, es können mehrere sein
  "background": {
    "scripts": ["background.js"]
  },

  // Welches HTML für das Popup verwendet werden soll
  "browser_action": {
    "default_title": "Meine Erweiterung",
    "default_popup": "popup.html"
  },

  // Inhalts-Skripte.
  // Wir haben ein Objekt: Für alle URLs, die mit http oder https beginnen, starten wir
  // den contenscript-Kontext mit dem Skript contentscript.js. Wird sofort beim Erhalt des Dokuments für alle Frames ausgeführt
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Zugriff auf localStorage und Idle-API erlaubt
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Hier werden die Ressourcen angegeben, auf die die Webseite zugreifen kann. Das heißt, sie können per fetch oder einfach xhr abgerufen werden
  "web_accessible_resources": ["inpage.js"]
}

Wir erstellen leere Dateien: background.js, popup.js, inpage.js und contentscript.js. Wir fügen popup.html hinzu - und unsere Anwendung kann bereits in Google Chrome geladen werden, um sicherzustellen, dass sie funktioniert.

Um sicherzugehen, kann man den Code nehmen. hier herunter. Darüber hinaus haben wir einen Link eingerichtet, um das Projekt mit webpack zu bauen. Um die Anwendung im Browser hinzuzufügen, muss man unter chrome://extensions die Option 'Entpackte Erweiterung laden' auswählen und den Ordner mit der entsprechenden Erweiterung – in unserem Fall dist – angeben.

Sichere Browsererweiterungen entwickeln

Jetzt ist unsere Erweiterung installiert und funktioniert. Die Entwicklertools für verschiedene Kontexte lassen sich folgendermaßen öffnen:

Popup ->

Sichere Browsererweiterungen entwickeln

Der Zugriff auf die Konsole des Content-Skripts erfolgt über die Konsole der Seite, auf der es ausgeführt wird.Sichere Browsererweiterungen entwickeln

Nachrichtenübertragung

Wir müssen also zwei Kommunikationskanäle einrichten: inpage <-> background und popup <-> background. Natürlich könnte man einfach Nachrichten an den Port senden und sein eigenes Protokoll erfinden, aber mir gefällt der Ansatz, den ich in einem Open-Source-Projekt von Metamask gesehen habe, besser.

Dies ist eine Browsererweiterung für die Arbeit mit dem Ethereum-Netzwerk. In dieser kommunizieren die verschiedenen Teile der Anwendung über RPC mithilfe der dnode-Bibliothek. Sie ermöglicht einen recht schnellen und komfortablen Austausch, wenn man ihr als Transport nodejs stream (also ein Objekt, das dasselbe Interface implementiert) bereitstellt:

import Dnode from "dnode/browser";

// In diesem Beispiel nehmen wir an, dass der Client Funktionen auf dem Server remote aufruft, obwohl uns nichts daran hindert, dies bidirektional zu gestalten.

// Server
// API, die wir bereitstellen möchten
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Transport, über den dnode arbeitet. Jeder Node.js Stream. Im Browser gibt es die Bibliothek 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Client
const dnodeClient = Dnode() // Aufruf ohne Argument bedeutet, dass wir keine API auf der anderen Seite bereitstellen

// Gibt "world" in der Konsole aus
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

Jetzt erstellen wir eine Anwendungs-Klasse. Diese wird API-Objekte für Popup und Webseite erstellen sowie für diese ein dnode erstellen:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Gibt das API-Objekt für die Benutzeroberfläche zurück
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Gibt das API-Objekt für die Seite zurück
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Verbindet die Popup-Benutzeroberfläche
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(remote)
        })
    }

    // Verbindet die Seite
    connectPage(connectionStream, origin){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(origin);
            console.log(remote)
        })
    }
}

Hier und im Folgenden verwenden wir anstelle des globalen Objekts Chrome das extentionApi, das auf Chrome im Google-Browser und auf browser in anderen zugreift. Dies wird zur Unterstützung der Cross-Browser-Kompatibilität gemacht, aber im Rahmen dieses Artikels könnte man auch einfach 'chrome.runtime.connect' verwenden.

Erstellen wir eine Instanz der Anwendung im Hintergrund-Skript:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const app = new SignerApp();

// onConnect wird bei der Verbindung von 'Prozessen' (Contentscript, Popup oder Erweiterungsseite) ausgelöst
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // Bei der Verbindungsherstellung kann ein Name angegeben werden, mit dem wir feststellen, wer sich verbunden hat, ob Contentscript oder UI
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

Da dnode mit Streams arbeitet und wir einen Port erhalten, ist eine Adapterklasse erforderlich. Diese wurde mit der Bibliothek readable-stream erstellt, die Node.js-Streams im Browser implementiert:

import {Duplex} from 'readable-stream';

export class PortStream extends Duplex{
    constructor(port){
        super({objectMode: true});
        this._port = port;
        port.onMessage.addListener(this._onMessage.bind(this));
        port.onDisconnect.addListener(this._onDisconnect.bind(this))
    }

    _onMessage(msg) {
        if (Buffer.isBuffer(msg)) {
            delete msg._isBuffer;
            const data = new Buffer(msg);
            this.push(data)
        } else {
            this.push(msg)
        }
    }

    _onDisconnect() {
        this.destroy()
    }

    _read(){}

    _write(msg, encoding, cb) {
        try {
            if (Buffer.isBuffer(msg)) {
                const data = msg.toJSON();
                data._isBuffer = true;
                this._port.postMessage(data)
            } else {
                this._port.postMessage(msg)
            }
        } catch (err) {
            return cb(new Error('PortStream - disconnected'))
        }
        cb()
    }
}

Jetzt erstellen wir die Verbindung in der UI:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import Dnode from 'dnode/browser';

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi(){
    // Auch hier, wie im Anwendungs-Klassen, erstellen wir den Port, wickeln ihn in einen Stream und machen Dnode
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    const background = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Machen Sie das API-Objekt über die Konsole zugänglich
    if (DEV_MODE){
        global.background = background;
    }
}

Dann erstellen wir eine Verbindung im Content-Skript:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import PostMessageStream from 'post-message-stream';

setupConnection();
injectScript();

function setupConnection(){
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // in-page Skript injizieren
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injection fehlgeschlagen.', e);
    }
}

Da wir die API nicht im Content-Skript, sondern direkt auf der Seite benötigen, unternehmen wir zwei Schritte:

  1. Wir erstellen zwei Streams. Einen — in Richtung Seite, über postMessage. Dafür verwenden wir dieses Paket von den Machern von Metamask. Der zweite Stream — zum Hintergrund über den erhaltenen Port von runtime.connect. Wir pipen sie. Jetzt hat die Seite einen Stream zum Hintergrund.
  2. Wir injizieren das Skript in den DOM. Wir laden das Skript herunter (der Zugriff darauf wurde im Manifest genehmigt) und erstellen ein Tag script mit seinem Inhalt darin:

import PostMessageStream from 'post-message-stream';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";

setupConnection();
injectScript();

function setupConnection(){
    // Verbindung zum Hintergrund
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    // Verbindung zur Seite
    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // Skript in die Seite einfügen
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Einfügen fehlgeschlagen.', e);
    }
}

Jetzt erstellen wir ein API-Objekt in der Inpage und machen es global:

import PostMessageStream from 'post-message-stream';
import Dnode from 'dnode/browser';

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Verbindung zum Contentscript
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    // API-Objekt erhalten
    const pageApi = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Zugriff über window
    global.SignerApp = pageApi;
}

Wir sind bereit Remote Procedure Call (RPC) mit eigenem API für Seite und UI. Wenn wir eine neue Seite mit dem Hintergrund verbinden, können wir das sehen:

Sichere Browsererweiterungen entwickeln

Leeres API und Origin. Auf der Seitenseite können wir die Funktion hello so aufrufen:

Sichere Browsererweiterungen entwickeln

Mit Callback-Funktionen in modernem JS zu arbeiten, ist nicht mehr zeitgemäß. Daher schreiben wir einen kleinen Helper, um ein dnode zu erstellen, das es ermöglicht, das API-Objekt in utils zu übergeben.

API-Objekte werden jetzt folgendermaßen aussehen:

export class SignerApp {

    popupApi() {
        return {
            hello: async () => "world"
        }
    }

...

}

Das Objekt von remote so erhalten:

import {cbToPromise, transformMethods} from "../../src/utils/setupDnode";

const pageApi = await new Promise(resolve => {
    dnode.once('remote', remoteApi => {
        // Mit Hilfe von Utilities ändern wir alle Callbacks in Promises
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

Und der Aufruf von Funktionen gibt ein Promise zurück:

Sichere Browsererweiterungen entwickeln

Die Version mit asynchronen Funktionen ist verfügbar hier.

Insgesamt scheint der Ansatz mit RPC und Streams ziemlich flexibel zu sein: Wir können Stream-Multiplexing verwenden und mehrere verschiedene APIs für unterschiedliche Aufgaben erstellen. Prinzipiell kann dnode überall verwendet werden, wichtig ist nur, den Transport in Form eines Node.js-Streams zu umhüllen.

Eine Alternative ist das JSON-Format, das das JSON RPC 2-Protokoll implementiert. Allerdings funktioniert es nur mit bestimmten Transporten (TCP und HTTP(S)), was in unserem Fall nicht anwendbar ist.

Interner Zustand und localStorage

Wir müssen den internen Zustand der Anwendung speichern – mindestens die Schlüssel für die Signatur. Es ist relativ einfach, den Zustand der Anwendung hinzuzufügen und Methoden zu deren Änderung in die Popup-API zu integrieren:

import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(){
        this.store = {
            keys: [],
        };
    }

    addKey(key){
        this.store.keys.push(key)
    }

    removeKey(index){
        this.store.keys.splice(index,1)
    }

    popupApi(){
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index)
        }
    }

    ...

} 

Im Hintergrund verpacken wir alles in eine Funktion und speichern das Anwendungsobjekt im window, damit wir es über die Konsole steuern können:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const app = new SignerApp();

    if (DEV_MODE) {
        global.app = app;
    }

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url;
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Fügen wir über die Konsole einige Schlüssel zur Benutzeroberfläche hinzu und sehen wir uns an, was mit dem Zustand passiert ist:

Sichere Browsererweiterungen entwickeln

Der Zustand muss persistent gemacht werden, damit die Schlüssel beim Neustart nicht verloren gehen.

Wir werden im localStorage speichern und bei jeder Änderung überschreiben. Zukünftig wird auch der Zugriff darauf für die Benutzeroberfläche benötigt, und wir möchten uns ebenfalls auf Änderungen abonnieren. Daher wäre es vorteilhaft, einen beobachtbaren Speicher (observable storage) zu erstellen und uns auf dessen Änderungen zu abonnieren.

Wir verwenden die Bibliothek mobx (https://github.com/mobxjs/mobx). Die Entscheidung fiel darauf, da wir bisher nicht mit ihr gearbeitet haben, aber großes Interesse daran hatten, sie zu lernen.

Wir fügen die Initialisierung des anfänglichen Zustands hinzu und machen den Store observable:

import {observable, action} from 'mobx';
import {setupDnode} from './utils/setupDnode';

export class SignerApp {

    constructor(initState = {}) {
        // Äußerlich bleibt der Store dasselbe Objekt, nur dass jetzt alle seine Felder Proxys sind, die auf Zugriffe überwachen
        this.store = observable.object({
            keys: initState.keys || [],
        });
    }

    // Methoden, die observable ändern, sollten üblicherweise mit einem Dekorator umschlossen werden
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

    @action
    removeKey(index) {
        this.store.keys.splice(index, 1)
    }

    ...

}

«Unter der Haube» hat mobx alle Felder des Stores durch Proxys ersetzt und überwacht alle Zugriffe darauf. Auf diese Zugriffe kann man abonnieren.

Ich werde häufig den Begriff „bei Änderung“ verwenden, auch wenn das nicht ganz korrekt ist. Mobx überwacht tatsächlich den Zugriff auf Felder. Hierbei kommen Getter und Setter von Proxy-Objekten zum Einsatz, die von der Bibliothek erstellt werden.

Die Action-Dekoratoren dienen zwei Zwecken:

  1. Im Strict-Modus mit dem Flag enforceActions verbietet Mobx direkte Änderungen am State. Es wird als guter Stil angesehen, im Strict-Modus zu arbeiten.
  2. Selbst wenn eine Funktion den State mehrmals ändert – zum Beispiel, wenn wir mehrere Felder in mehreren Codezeilen ändern – werden die Observer nur nach deren Abschluss benachrichtigt. Dies ist besonders wichtig im Frontend, wo unnötige State-Updates zu überflüssigem Rendern von Elementen führen. In unserem Fall sind weder das Erste noch das Zweite besonders relevant, jedoch wollen wir uns an die besten Praktiken halten. Dekoratoren werden typischerweise auf alle Funktionen angewendet, die den State von beobachtbaren Feldern ändern.

Hintergrundmäßig fügen wir die Initialisierung und Speicherung des States in localStorage hinzu:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
// Hilfsfunktionen zum Speichern/Laden eines Objekts in/von localStorage als JSON-String unter dem Schlüssel 'store'
import {loadState, saveState} from "./utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Einrichten der Zustandspersistenz

    // Das Ergebnis der Reaktion wird einer Variablen zugewiesen, damit die Abonnierung abgebrochen werden kann. Dies ist nicht nötig, bleibt jedoch als Beispiel erhalten.
    const localStorageReaction = reaction(
        () => toJS(app.store), // Daten-Selektor-Funktion
        saveState // Funktion, die bei einer Änderung der Daten, die der Selektor zurückgibt, aufgerufen wird
    );

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url;
            app.connectPage(portStream, origin);
        } else {
            app.connectPopup(portStream);
        }
    }
}

Hier ist die Funktion reaction von Interesse. Sie hat zwei Argumente:

  1. Daten-Selektor.
  2. Handler, der mit diesen Daten jedes Mal aufgerufen wird, wenn sie sich ändern.

Im Gegensatz zu Redux, wo wir den Zustand explizit als Argument erhalten, merkt sich MobX, auf welche Observable wir innerhalb des Selectors zugreifen, und ruft den Handler nur bei deren Änderung auf.

Es ist wichtig zu verstehen, wie MobX bestimmt, auf welche Observables wir uns abonnieren. Wenn ich den Selector so geschrieben hätte() => app.store, würde die Reaction nie ausgelöst, da das Store selbst nicht beobachtbar ist, sondern nur dessen Felder.

Hätte ich es so geschrieben () => app.store.keys, würde wieder nichts passieren, da sich bei Hinzufügen/Entfernen von Array-Elementen die Referenz darauf nicht ändert.

MobX führt die Selector-Funktion beim ersten Mal aus und beobachtet nur die Observables, auf die wir zugegriffen haben. Dies geschieht über Proxy-Getter. Daher wird hier die eingebaute Funktion toJS. verwendet. Sie gibt ein neues Objekt zurück, in dem alle Proxys durch die Originalfelder ersetzt werden. Während der Ausführung werden alle Felder des Objekts gelesen – folglich werden die Getter aktiviert.

Im Popup der Konsole fügen wir erneut einige Schlüssel hinzu. Diesmal sind sie auch im localStorage gelandet:

Sichere Browsererweiterungen entwickeln

Beim neu Laden der Hintergrundseite bleibt die Information an Ort und Stelle.

Der gesamte Anwendungscode bis zu diesem Punkt ist einsehbar. hier.

Sichere Speicherung von privaten Schlüsseln.

Es ist unsicher, private Schlüssel im Klartext zu speichern: Es besteht immer die Möglichkeit, dass Sie gehackt werden, jemand Zugang zu Ihrem Computer erhält usw. Deshalb werden wir die Schlüssel in localStorage verschlüsselt speichern.

Für mehr Sicherheit fügen wir der Anwendung den Status "locked" hinzu, in dem der Zugang zu den Schlüsseln komplett verweigert wird. Wir werden das Plugin automatisch nach einer Zeitüberschreitung in den Status "locked" versetzen.

MobX ermöglicht die Speicherung nur einer minimalen Menge von Daten, während der Rest automatisch auf dieser Basis berechnet wird. Dies sind die sogenannten computed properties. Sie können mit Views in Datenbanken verglichen werden:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Hilfsmittel für die sichere Verschlüsselung von Strings. Verwenden crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Speichert das Passwort und die verschlüsselten Schlüssel. Wenn das Passwort null ist, ist die App gesperrt
            password: null,
            vault: initState.vault,

            // Getter für berechnete Felder. Man kann eine Analogie zu Views in der Datenbank ziehen.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Initialisierung eines leeren Speichers mit neuem Passwort
    @action
    initVault(password){
        this.store.vault = SignerApp._encryptVault([], password)
    }
    @action
    lock() {
        this.store.password = null
    }
    @action
    unlock(password) {
        this._checkPassword(password);
        this.store.password = password
    }
    @action
    addKey(key) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password)
    }
    @action
    removeKey(index) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault([
                ...this.store.keys.slice(0, index),
                ...this.store.keys.slice(index + 1)
            ],
            this.store.password
        )
    }

    ... // Code für die Verbindung und API

    // privat
    _checkPassword(password) {
        SignerApp._decryptVault(this.store.vault, password);
    }

    _checkLocked() {
        if (this.store.locked){
            throw new Error('App ist gesperrt')
        }
    }

    // Methoden für die Verschlüsselung/Entschlüsselung des Speichers
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Speicher nicht initialisiert')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Falsches Passwort')
        }
    }
}

Jetzt speichern wir nur noch verschlüsselte Schlüssel und Passwörter. Alles andere wird berechnet. Der Übergang in den Zustand "locked" erfolgt durch das Entfernen des Passworts aus dem Zustand. Im öffentlichen API wurde eine Methode zur Initialisierung des Speichers hinzugefügt.

Für die Verschlüsselung wurden Utilities unter Verwendung von crypto-js erstellt.:

import CryptoJS from 'crypto-js'

// Wird verwendet, um das Knacken des Passworts durch Brute-Force-Angriffe zu erschweren. Für jede Passwortvariante muss der Angreifer 5000 Hashes erstellen.
function strengthenPassword(pass, rounds = 5000) {
    while (rounds-- > 0){
        pass = CryptoJS.SHA256(pass).toString()
    }
    return pass
}

export function encrypt(str, pass){
    const strongPass = strengthenPassword(pass);
    return CryptoJS.AES.encrypt(str, strongPass).toString()
}

export function decrypt(str, pass){
    const strongPass = strengthenPassword(pass)
    const decrypted = CryptoJS.AES.decrypt(str, strongPass);
    return decrypted.toString(CryptoJS.enc.Utf8)
}

Der Browser verfügt über eine Idle-API, über die man auf das Ereignis – Zustandsänderungen – abonnieren kann. Der Zustand kann demnach Im Leerlauf, aktiv und locked. Für idle kann ein Timeout festgelegt werden, während locked aktiviert wird, wenn das Betriebssystem selbst gesperrt ist. Außerdem werden wir den Selektor für das Speichern in localStorage ändern:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
import {loadState, saveState} from "./utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';
const IDLE_INTERVAL = 30;

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Jetzt rufen wir explizit das Feld auf, auf das zugegriffen werden soll; reaction wird ordnungsgemäß funktionieren
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Zeitüberschreitung bei Inaktivität, wenn ein Ereignis ausgelöst wird
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Wenn der Benutzer den Bildschirm gesperrt hat oder während des angegebenen Intervalls inaktiv war, sperren wir die Anwendung
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Verbindung zu anderen Kontexten
    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Der Code bis zu diesem Schritt befindet sich hier.

Transaktionen

Nun kommen wir zum Wesentlichen: dem Erstellen und Signieren von Transaktionen in der Blockchain. Wir werden die WAVES-Blockchain und die Bibliothek verwenden waves-transactions.

Zuerst fügen wir dem Zustand ein Array von Nachrichten hinzu, die signiert werden müssen, gefolgt von Methoden zum Hinzufügen neuer Nachrichten, zur Bestätigung der Signatur und zum Widerrufen:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from './utils/setupDnode';
import {decrypt, encrypt} from './utils/cryptoUtils';

export class SignerApp {

    ...

    @action
    newMessage(data, origin) {
        // Für jede Nachricht erstellen wir Metadaten mit ID, Status, Erstellungszeit usw.
        const message = observable.object({
            id: uuid(), // Identifikator, verwende uuid
            origin, // Origin werden wir später in der Benutzeroberfläche anzeigen
            data, //
            status: 'new', // Es wird vier Status geben: new, signed, rejected und failed
            timestamp: Date.now()
        });
        console.log(`neue Nachricht: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Wir geben ein Versprechen zurück, in dem mobx die Änderungen an der Nachricht überwacht. Sobald sich der Status ändert, werden wir es auflösen
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, // Wir werden den Status der Nachricht beobachten
                (status, reaction) => { // der zweite Parameter ist ein Verweis auf die Reaktion, damit sie innerhalb des Aufrufs zerstört werden kann
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('Benutzer hat die Nachricht abgelehnt'));
                            break;
                        case 'failed':
                            reject(new Error(message.err.message));
                            break;
                        default:
                            return
                    }
                    reaction.dispose()
                }
            )
        })
    }
    @action
    approve(id, keyIndex = 0) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`Keine Nachricht mit ID:${id}`);
        try {
            message.data = signTx(message.data, this.store.keys[keyIndex]);
            message.status = 'signed'
        } catch (e) {
            message.err = {
                stack: e.stack,
                message: e.message
            };
            message.status = 'failed'
            throw e
        }
    }
    @action
    reject(id) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`Keine Nachricht mit ID:${id}`);
        message.status = 'rejected'
    }

    ...
}

Beim Empfang einer neuen Nachricht fügen wir Metadaten hinzu, erstellen sie beobachtbar und fügen sie in store.messages.

Wenn wir das nicht manuell erledigen, wird mobx dies beim Hinzufügen zum Array messages selbst tun. Allerdings wird es ein neues Objekt erstellen, auf das wir keinen Zugriff haben, was für den nächsten Schritt erforderlich ist. beobachtbar Anschließend geben wir ein Promise zurück, das bei Änderung des Status der Nachricht aufgelöst wird. Der Status wird von der reaction überwacht, die sich selbst «beendet», wenn sich der Status ändert.

Die Implementierung der Methoden

approve reject und ist sehr einfach: Wir ändern einfach den Status der Nachricht, nachdem wir ihn gegebenenfalls signiert haben. Approve und reject integrieren wir in die API UI, newMessage in die API der Seite:

export class SignerApp { ... popupApi() { return { addKey: async (key) => this.addKey(key), removeKey: async (index) => this.removeKey(index),lock: async () => this.lock(), unlock: async (password) => this.unlock(password), initVault: async (password) => this.initVault(password),approve: async (id, keyIndex) => this.approve(id, keyIndex), reject: async (id) => this.reject(id) } }pageApi(origin) { return { signTransaction: async (txParams) => this.newMessage(txParams, origin) } }... }

Jetzt versuchen wir, die Transaktion mit der Erweiterung zu signieren:

Insgesamt ist alles bereit, es bleibt nur noch

Sichere Browsererweiterungen entwickeln

eine einfache Benutzeroberfläche hinzuzufügen добавить простой UI.

UI

Die Benutzeroberfläche benötigt Zugriff auf den Anwendungsstatus. Wir werden den Status auf der UI-Seite erstellen und eine API-Funktion hinzufügen, die diesen Status ändert. Wir fügen hinzu beobachtbar zum API-Objekt, das von der Hintergrundanwendung erhalten wurde: beobachtbar import {observable} from 'mobx' import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode"; import {initApp} from "./ui/index";const DEV_MODE = process.env.NODE_ENV !== 'production';setupUi().catch(console.error);async function setupUi() { // Wir stellen eine Verbindung zum Port her und erstellen einen Stream const backgroundPort = extensionApi.runtime.connect({name: 'popup'}); const connectionStream = new PortStream(backgroundPort);// Wir erstellen ein leeres Observable für den Status des Hintergrunds let backgroundState = observable.object({}); const api = { // Wir geben der Hintergrundanwendung eine Funktion, die das Observable aktualisiert updateState: async state => { Object.assign(backgroundState, state) } };// Wir erstellen ein RPC-Objekt const dnode = setupDnode(connectionStream, api); const background = await new Promise(resolve => { dnode.once('remote', remoteApi => { resolve(transformMethods(cbToPromise, remoteApi)) }) });// Wir fügen dem Hintergrund ein Observable mit dem Status hinzu background.state = backgroundState;if (DEV_MODE) { global.background = background; }// Start der Benutzeroberfläche await initApp(background) }

import {observable} from 'mobx'
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode";
import {initApp} from "./ui/index";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi() {
    // Подключаемся к порту, создаем из него стрим
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    // Создаем пустой observable для состояния background'a
    let backgroundState = observable.object({});
    const api = {
        //Отдаем бекграунду функцию, которая будет обновлять observable
        updateState: async state => {
            Object.assign(backgroundState, state)
        }
    };

    // Делаем RPC объект
    const dnode = setupDnode(connectionStream, api);
    const background = await new Promise(resolve => {
        dnode.once('remote', remoteApi => {
            resolve(transformMethods(cbToPromise, remoteApi))
        })
    });

    // Добавляем в background observable со стейтом
    background.state = backgroundState;

    if (DEV_MODE) {
        global.background = background;
    }

    // Запуск интерфейса
    await initApp(background)
}

Am Ende starten wir das Rendern der Benutzeroberfläche der Anwendung. Es handelt sich um eine React-Anwendung. Das Background-Objekt wird einfach über Props übergeben. Es wäre natürlich besser, einen separaten Service für die Methoden und einen Store für den Zustand zu erstellen, aber im Rahmen dieses Artikels ist das ausreichend:

import {render} from 'react-dom'
import App from './App'
import React from "react";

// Initialisieren der Anwendung mit dem Background-Objekt als Props
export async function initApp(background){
    render(
        ,
        document.getElementById('app-content')
    );
}

Mit MobX ist es sehr einfach, das Rendern bei Datenänderungen zu starten. Wir fügen einfach den Observer-Dekorator aus dem Paket mobx-react zum Komponenten hinzu, und das Rendern wird automatisch aufgerufen, wenn sich die Observable, auf die sich die Komponente bezieht, ändert. Es sind keine mapStateToProps oder connect wie in Redux erforderlich. Alles funktioniert sofort "out of the box":

import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from './components/Initialize'
import Keys from './components/Keys'
import Sign from './components/Sign'
import Unlock from './components/Unlock'

@observer // Mit diesem Decorator wird die render-Methode automatisch aufgerufen, wenn sich Observable, auf die verwiesen wird, ändern.
export default class App extends Component {

    // Es wäre natürlich besser, die Logik für den Seitenrendering in das Routing auszulagern und keine verschachtelten ternären Operatoren zu verwenden,
    // und die Observable und Methoden direkt mit den Komponenten zu verbinden, die sie nutzen.
    render() {
        const {keys, messages, initialized, locked} = this.props.background.state;
        const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;

        return <fragment>
            {!initialized
                ?
                <init oninit="{initVault}/">
                :
                locked
                    ?
                    <unlock onunlock="{unlock}/">
                    :
                    messages.length &gt; 0
                        ?
                        <sign keys="{keys}" message="{messages[messages.length" - 1]} onapprove="{approve}" onreject="{reject}/">
                        :
                        <keys keys="{keys}" onadd="{addKey}" onremove="{removeKey}/">
            }
            <div>
                {!locked &amp;&amp; <button onclick="{()" > lock()}&gt;App sperren</button>}
                {initialized &amp;&amp; <button onclick="{()" > deleteVault()}&gt;Alle Schlüssel löschen und zurücksetzen</button>}
            </div>
        </Fragment>
    }
}

Die anderen Komponenten können im Code betrachtet werden im UI-Ordner.

Jetzt müssen wir in der Anwendungsklasse einen Zustand-Seletor für das UI erstellen und es bei Änderungsbenachrichtigungen informieren. Dafür fügen wir die Methode hinzu getState und reaction, die remote.updateState:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from './utils/setupDnode';
import {decrypt, encrypt} from './utils/cryptoUtils';

export class SignerApp {

    ...

    // öffentlich
    getState() {
        return {
            keys: this.store.keys,
            messages: this.store.newMessages,
            initialized: this.store.initialized,
            locked: this.store.locked
        }
    }

    ...

    //
    connectPopup(connectionStream) {
        const api = this.popupApi();
        const dnode = setupDnode(connectionStream, api);

        dnode.once('remote', (remote) => {
            // Reaktion auf Änderungen des States erstellen, die die remote Prozedur aufruft und den State im UI-Prozess aktualisiert
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Im dritten Argument können Parameter übergeben werden. fireImmediately bedeutet, dass die Reaktion beim ersten Mal sofort ausgeführt wird.
                // Dies ist notwendig, um den Anfangszustand zu erhalten. Delay ermöglicht die Einrichtung eines Debounce
                {fireImmediately: true, delay: 500}
            );
            // Abonnierung bei Beendigung des Clients entfernen
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Beim Empfang des Objekts remote wird erstellt reaction auf die Änderung des States, der eine Funktion auf der UI-Seite aufruft.

Der letzte Schliff – wir fügen die Anzeige neuer Nachrichten auf dem Erweiterungssymbol hinzu:

function setupApp() {
...

    // Reaktion auf das Setzen des Badge-Texts.
    reaction(
        () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
        text => extensionApi.browserAction.setBadgeText({text}),
        {fireImmediately: true}
    );

...
}

So, die Anwendung ist bereit. Webseiten können nun Transaktionssignaturen anfordern:

Sichere Browsererweiterungen entwickeln

Sichere Browsererweiterungen entwickeln

Der Code ist verfügbar unter diesem über diesen Link verfügbar.

Fazit

Wenn Sie den Artikel bis zum Ende gelesen haben, aber noch Fragen haben, können Sie diese in dem Repository des Erweiterung. Dort finden Sie auch die Commits zu jedem markierten Schritt.

Und wenn Sie interessiert sind, den Code einer echten Erweiterung zu sehen, können Sie dies hier.

Code, Repository und Arbeitsbeschreibung von siemarell

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster