Ekri yon ekstansyon navigatè sekirite

Ekri yon ekstansyon navigatè sekirite

Kontrèman ak achitekti komen "kliyan-sèvè", aplikasyon desantralize yo karakterize pa:

  • Pa gen okenn nesesite pou estoke yon baz done ak logins itilizatè yo ak modpas yo. Aksè enfòmasyon yo estoke sèlman pa itilizatè yo tèt yo, ak konfimasyon otantisite yo rive nan nivo pwotokòl la.
  • Pa bezwen sèvi ak yon sèvè. Lojik aplikasyon an ka egzekite sou yon rezo blockchain, kote li posib pou estoke kantite done ki nesesè yo.

Gen 2 depo relativman an sekirite pou kle itilizatè yo - bous pyès ki nan konpitè ak ekstansyon navigatè. Bous pyès ki nan konpitè yo sitou trè an sekirite, men difisil pou itilize epi yo lwen gratis, men ekstansyon navigatè yo se konbinezon pafè a nan sekirite ak fasilite nan itilize, epi yo ka tou konplètman gratis pou itilizatè fen yo.

Lè nou pran tout bagay sa yo an kont, nou te vle fè ekstansyon ki pi an sekirite ki senplifye devlopman aplikasyon desantralize lè nou bay yon API senp pou travay ak tranzaksyon ak siyati.
Nou pral di w sou eksperyans sa a anba a.

Atik la pral gen enstriksyon etap pa etap sou kòman yo ekri yon ekstansyon navigatè, ak egzanp kòd ak Ekran. Ou ka jwenn tout kòd la nan depo. Chak komèt lojikman koresponn ak yon seksyon nan atik sa a.

Yon istwa kout nan ekstansyon navigatè

Ekstansyon navigatè yo te alantou pou yon tan long. Yo te parèt nan Internet Explorer tounen nan 1999, nan Firefox nan 2004. Sepandan, pou yon tan trè lontan pa te gen okenn estanda sèl pou ekstansyon.

Nou ka di ke li te parèt ansanm ak ekstansyon nan katriyèm vèsyon Google Chrome. Natirèlman, pa te gen okenn spesifikasyon lè sa a, men li te Chrome API a ki te vin baz li yo: li te konkeri pi fò nan mache a navigatè ak gen yon magazen aplikasyon bati-an, Chrome aktyèlman mete estanda a pou ekstansyon navigatè.

Mozilla te gen estanda pwòp li yo, men wè popilarite ekstansyon Chrome, konpayi an deside fè yon API konpatib. Nan 2015, nan inisyativ Mozilla, yo te kreye yon gwoup espesyal nan World Wide Web Consortium (W3C) pou travay sou espesifikasyon ekstansyon kwa-navigatè.

Yo te pran ekstansyon API ki egziste deja pou Chrome kòm yon baz. Travay la te fèt ak sipò Microsoft (Google te refize patisipe nan devlopman estanda a), e kòm yon rezilta, yon bouyon parèt. espesifikasyon.

Fòmèlman, se Edge, Firefox ak Opera ki sipòte spesifikasyon (remake ke Chrome pa sou lis sa a). Men, an reyalite, estanda a se lajman konpatib ak Chrome, paske li se aktyèlman ekri ki baze sou ekstansyon li yo. Ou ka li plis enfòmasyon sou API WebExtensions isit la.

Estrikti ekstansyon

Sèl dosye ki nesesè pou ekstansyon an se manifest (manifest.json). Li se tou "pwen antre" nan ekspansyon an.

Manifès

Dapre spesifikasyon la, dosye manifest la se yon dosye JSON ki valab. Yon deskripsyon konplè kle manifeste ak enfòmasyon sou ki kle yo sipòte nan ki navigatè ka wè isit la.

Kle ki pa nan spesifikasyon "ka" dwe inyore (tou de Chrome ak Firefox rapòte erè, men ekstansyon yo kontinye travay).

E mwen ta renmen atire atansyon sou kèk pwen.

  1. background — yon objè ki gen ladann jaden sa yo:
    1. Scripts — yon etalaj de scripts ki pral egzekite nan yon kontèks background (nou pral pale sou sa a yon ti kras pita);
    2. paj - olye pou yo scripts ki pral egzekite nan yon paj vid, ou ka presize html ak kontni. Nan ka sa a, yo pral inyore jaden script la, epi scripts yo pral bezwen mete nan paj kontni an;
    3. pèsiste — yon drapo binè, si li pa espesifye, navigatè a pral "touye" pwosesis background nan lè li konsidere ke li pa fè anyen, epi rekòmanse li si sa nesesè. Sinon, paj la pral sèlman dechaje lè navigatè a fèmen. Pa sipòte nan Firefox.
  2. content_scripts — yon etalaj de objè ki pèmèt ou chaje diferan scripts nan diferan paj wèb. Chak objè gen jaden enpòtan sa yo:
    1. alimèt - url modèl, ki detèmine si yon script kontni patikilye pral enkli oswa ou pa.
    2. js — yon lis scripts ki pral chaje nan match sa a;
    3. exclude_matches - ekskli nan jaden an match URL ki koresponn ak jaden sa a.
  3. page_action - se aktyèlman yon objè ki responsab pou icon nan ki parèt akote ba adrès la nan navigatè a ak entèraksyon ak li. Li pèmèt ou tou montre yon fenèt popup, ki defini lè l sèvi avèk pwòp HTML, CSS ak JS ou.
    1. default_popup — chemen nan fichye HTML ak koòdone popup la, ka genyen CSS ak JS.
  4. autorisations — yon etalaj pou jere dwa ekstansyon. Gen 3 kalite dwa, ki dekri an detay isit la
  5. web_accessible_resources — resous ekstansyon ke yon paj wèb ka mande, pou egzanp, imaj, JS, CSS, dosye HTML.
  6. deyò_konekte — isit la ou ka klèman presize idantite lòt ekstansyon ak domèn paj wèb kote ou ka konekte. Yon domèn ka dezyèm nivo oswa pi wo. Pa travay nan Firefox.

Kontèks ekzekisyon

Ekstansyon an gen twa kontèks ekzekisyon kòd, se sa ki, aplikasyon an konsiste de twa pati ak diferan nivo aksè nan API navigatè a.

Kontèks ekstansyon

Pifò nan API a disponib isit la. Nan kontèks sa a yo "viv":

  1. Paj background - "backend" pati nan ekstansyon an. Fichye a espesifye nan manifest la lè l sèvi avèk kle "background" la.
  2. Popup paj — yon paj popup ki parèt lè w klike sou ikòn ekstansyon an. Nan manifeste a browser_action -> default_popup.
  3. Paj Custom — paj ekstansyon, "k ap viv" nan yon tab separe nan gade nan chrome-extension://<id_расширения>/customPage.html.

Kontèks sa a egziste poukont li nan fenèt navigatè ak onglè yo. Paj background egziste nan yon kopi sèl epi li toujou ap travay (eksepsyon an se paj evènman an, lè script background nan lanse pa yon evènman ak "mouri" apre ekzekisyon li). Popup paj egziste lè fenèt popup la louvri, ak Paj Custom — pandan tab la ak li louvri. Pa gen aksè a lòt onglet ak sa yo nan kontèks sa a.

Kontèks script kontni

Fichye script kontni an te lanse ansanm ak chak tab navigatè. Li gen aksè a yon pati nan API ekstansyon an ak pyebwa DOM nan paj wèb la. Se scripts kontni ki responsab pou entèraksyon ak paj la. Ekstansyon ki manipile pyebwa DOM fè sa nan scripts kontni - pou egzanp, bloke anons oswa tradiktè. Epitou, script kontni an ka kominike ak paj la atravè estanda postMessage.

Kontèks paj wèb

Sa a se paj entènèt aktyèl la tèt li. Li pa gen anyen fè ak ekstansyon an epi li pa gen aksè la, eksepte nan ka kote domèn nan paj sa a pa endike klèman nan manifest la (plis sou sa a anba a).

Echanj mesaj

Diferan pati nan aplikasyon an dwe fè echanj mesaj youn ak lòt. Gen yon API pou sa runtime.sendMessage pou voye yon mesaj background и tabs.sendMessage pou voye yon mesaj nan yon paj (script kontni, popup oswa paj wèb si sa disponib externally_connectable). Anba a se yon egzanp lè w ap jwenn aksè nan API Chrome.

// Сообщением может быть любой 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))
    }
)

Pou kominikasyon konplè, ou ka kreye koneksyon atravè runtime.connect. Nan repons nou pral resevwa runtime.Port, kote, pandan ke li louvri, ou ka voye nenpòt kantite mesaj. Sou bò kliyan, pou egzanp, contentscript, li sanble sa a:

// Опять же 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"});

Sèvè oswa background:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, 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) {
    ...
});

Genyen tou yon evènman onDisconnect ak metòd disconnect.

Dyagram aplikasyon an

Ann fè yon ekstansyon navigatè ki estoke kle prive, bay aksè a enfòmasyon piblik (adrès, kle piblik kominike ak paj la epi pèmèt aplikasyon twazyèm pati yo mande yon siyati pou tranzaksyon yo.

Devlopman aplikasyon

Aplikasyon nou an dwe tou de kominike avèk itilizatè a epi bay paj la yon API pou rele metòd (pa egzanp, siyen tranzaksyon yo). Fè fè ak yon sèl contentscript pa pral travay, paske li sèlman gen aksè a DOM a, men se pa JS la nan paj la. Konekte atravè runtime.connect nou pa kapab, paske API a nesesè sou tout domèn, epi sèlman espesifik yo ka espesifye nan manifest la. Kòm yon rezilta, dyagram nan pral sanble sa a:

Ekri yon ekstansyon navigatè sekirite

Pral gen yon lòt script - inpage, ke nou pral enjekte nan paj la. Li pral kouri nan kontèks li epi li bay yon API pou travay ak ekstansyon an.

Kòmanse

Tout kòd ekstansyon navigatè disponib nan GitHub. Pandan deskripsyon an pral gen lyen ki mennen nan komèt.

Ann kòmanse ak manifest la:

{
  // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id=<id расширения>
  "name": "Signer",
  "description": "Extension demo",
  "version": "0.0.1",
  "manifest_version": 2,

  // Скрипты, которые будут исполнятся в background, их может быть несколько
  "background": {
    "scripts": ["background.js"]
  },

  // Какой html использовать для popup
  "browser_action": {
    "default_title": "My Extension",
    "default_popup": "popup.html"
  },

  // Контент скрипты.
  // У нас один объект: для всех url начинающихся с http или https мы запускаем
  // contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Разрешен доступ к localStorage и idle api
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr
  "web_accessible_resources": ["inpage.js"]
}

Kreye background.js vid, popup.js, inpage.js ak contentscript.js. Nou ajoute popup.html - epi aplikasyon nou an ka deja chaje nan Google Chrome epi asire w ke li fonksyone.

Pou verifye sa, ou ka pran kòd la kon sa. Anplis de sa nou te fè, lyen an te configured asanble pwojè a lè l sèvi avèk webpack. Pou ajoute yon aplikasyon nan navigatè a, nan chrome://extensions ou bezwen chwazi chaj depake ak katab la ak ekstansyon ki koresponn lan - nan ka nou an dist.

Ekri yon ekstansyon navigatè sekirite

Koulye a, ekstansyon nou an enstale ak travay. Ou ka kouri zouti pwomotè yo pou diferan kontèks jan sa a:

popup ->

Ekri yon ekstansyon navigatè sekirite

Aksè nan konsole script kontni an fèt atravè konsole paj li menm kote li te lanse a.Ekri yon ekstansyon navigatè sekirite

Echanj mesaj

Se konsa, nou bezwen etabli de chanèl kominikasyon: inpage <-> background ak popup <-> background. Ou ka, nan kou, jis voye mesaj nan pò a ak envante pwotokòl pwòp ou a, men mwen prefere apwòch la ke mwen te wè nan pwojè a sous louvri metamask.

Sa a se yon ekstansyon navigatè pou travay ak rezo Ethereum. Nan li, diferan pati nan aplikasyon an kominike atravè RPC lè l sèvi avèk bibliyotèk la dnode. Li pèmèt ou òganize yon echanj byen vit ak byen si ou bay li ak yon kouran nodejs kòm yon transpò (sa vle di yon objè ki aplike menm koòdone a):

import Dnode from "dnode/browser";

// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным

// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне

// Выведет в консоль world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

Koulye a, nou pral kreye yon klas aplikasyon. Li pral kreye objè API pou popup la ak paj wèb, epi kreye yon dnode pou yo:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Возвращает объект API для ui
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Возвращает объет API для страницы
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Подключает popup ui
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

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

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

    // Подключает страницу
    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)
        })
    }
}

Isit la ak anba a, olye pou yo objè Chrome mondyal la, nou itilize extensionApi, ki gen aksè nan Chrome nan navigatè Google la ak navigatè nan lòt moun. Sa a se fè pou konpatibilite kwa-navigatè, men pou rezon atik sa a yon moun ta ka tou senpleman itilize 'chrome.runtime.connect'.

Ann kreye yon egzanp aplikasyon nan script background nan:

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

const app = new SignerApp();

// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

Depi dnode travay ak kouran, epi nou resevwa yon pò, yo bezwen yon klas adaptè. Li fèt lè l sèvi avèk bibliyotèk lizib la, ki aplike kouran nodejs nan navigatè a:

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()
    }
}

Koulye a, ann kreye yon koneksyon nan UI a:

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(){
    // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем  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)
        })
    });

    // Делаем объект API доступным из консоли
    if (DEV_MODE){
        global.background = background;
    }
}

Lè sa a, nou kreye koneksyon an nan script kontni an:

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 {
        // inject in-page script
        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 failed.', e);
    }
}

Depi nou bezwen API a pa nan script kontni an, men dirèkteman sou paj la, nou fè de bagay:

  1. Nou kreye de kouran. Youn - nan direksyon pou paj la, sou tèt postMessage la. Pou sa nou itilize sa a pake sa a soti nan kreyatè yo nan metamask. Kouran an dezyèm se background sou pò a te resevwa nan men runtime.connect. Ann achte yo. Koulye a, paj la pral gen yon kouran nan background nan.
  2. Enjekte script la nan DOM la. Telechaje script la (yo te pèmèt aksè a li nan manifest la) epi kreye yon tag script ak kontni li yo andedan:

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

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 {
        // inject in-page script
        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 failed.', e);
    }
}

Koulye a, nou kreye yon objè api nan inpage epi mete li nan global:

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

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Стрим к контентскрипту
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

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

    // Получаем объект API
    const pageApi = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Доступ через window
    global.SignerApp = pageApi;
}

Nou pare Rele Pwosedi Remote (RPC) ak API separe pou paj ak UI. Lè w konekte yon nouvo paj nan background nou ka wè sa a:

Ekri yon ekstansyon navigatè sekirite

Vide API ak orijin. Sou bò paj la, nou ka rele fonksyon alo tankou sa a:

Ekri yon ekstansyon navigatè sekirite

Travay ak fonksyon callback nan JS modèn se move fason, kidonk ann ekri yon ti asistan pou kreye yon dnode ki pèmèt ou pase yon objè API bay utils.

Objè API yo pral kounye a sanble tankou sa a:

export class SignerApp {

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

...

}

Jwenn yon objè nan remote tankou sa a:

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

const pageApi = await new Promise(resolve => {
    dnode.once('remote', remoteApi => {
        // С помощью утилит меняем все callback на promise
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

Epi apèl fonksyon retounen yon pwomès:

Ekri yon ekstansyon navigatè sekirite

Vèsyon ak fonksyon asynchrone disponib isit la.

An jeneral, RPC ak apwòch kouran an sanble byen fleksib: nou ka itilize vapè multiplexing epi kreye plizyè API diferan pou travay diferan. Nan prensip, dnode ka itilize nenpòt kote, bagay prensipal la se vlope transpò a nan fòm lan nan yon kouran nodejs.

Yon altènatif se fòma JSON, ki aplike pwotokòl JSON RPC 2. Sepandan, li travay ak transpò espesifik (TCP ak HTTP(S)), ki pa aplikab nan ka nou an.

Entèn eta ak lokal Depo

Nou pral bezwen estoke eta entèn aplikasyon an - omwen kle yo siyen. Nou ka byen fasil ajoute yon eta nan aplikasyon an ak metòd pou chanje li nan popup API a:

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

    ...

} 

Nan background, nou pral vlope tout bagay nan yon fonksyon epi ekri objè aplikasyon an nan fenèt pou nou ka travay avèk li nan konsole a:

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

Ann ajoute kèk kle nan konsole UI a epi wè sa k ap pase ak eta a:

Ekri yon ekstansyon navigatè sekirite

Eta a dwe fè pèsistan pou kle yo pa pèdi lè rekòmanse.

Nou pral estoke li nan localStorage, ranplase li ak chak chanjman. Apre sa, aksè a li pral nesesè tou pou UI a, epi mwen ta renmen tou abònman nan chanjman. Ki baze sou sa a, li pral pratik yo kreye yon depo obsèvab ak abònman nan chanjman li yo.

Nou pral itilize bibliyotèk mobx (https://github.com/mobxjs/mobx). Chwa a te tonbe sou li paske mwen pa t 'gen travay avèk li, men mwen te reyèlman vle etidye li.

Ann ajoute inisyalizasyon eta inisyal la epi fè magazen an obsèvab:

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

export class SignerApp {

    constructor(initState = {}) {
        // Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним
        this.store =  observable.object({
            keys: initState.keys || [],
        });
    }

    // Методы, которые меняют observable принято оборачивать декоратором
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

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

    ...

}

"Anba kapo a," mobx te ranplase tout jaden magazen ak prokurasyon ak entèsepte tout apèl yo ba yo. Li pral posib pou abònman ak mesaj sa yo.

Anba a mwen pral souvan itilize tèm nan "lè chanje", byenke sa a pa totalman kòrèk. Mobx swiv aksè nan jaden yo. Getters ak setters nan objè proxy ke bibliyotèk la kreye yo itilize.

Dekoratè aksyon sèvi de rezon:

  1. Nan mòd strik ak drapo enforceActions, mobx entèdi chanje eta dirèkteman. Li konsidere kòm bon pratik pou travay nan kondisyon strik.
  2. Menm si yon fonksyon chanje eta a plizyè fwa - pou egzanp, nou chanje plizyè jaden nan plizyè liy nan kòd - obsèvatè yo notifye sèlman lè li fini. Sa a se espesyalman enpòtan pou entèfas a, kote mizajou eta a pa nesesè mennen nan rann nesesè nan eleman. Nan ka nou an, ni premye a ni dezyèm lan pa patikilyèman enpòtan, men nou pral swiv pi bon pratik yo. Li se nòmal yo tache dekoratè nan tout fonksyon ki chanje eta a nan jaden yo obsève.

Nan background nan nou pral ajoute inisyalizasyon ak ekonomize eta a nan localStorage:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
// Вспомогательные методы. Записывают/читают объект в/из localStorage виде JSON строки по ключу '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;
    }

    // Setup state persistence

    // Результат reaction присваивается переменной, чтобы подписку можно было отменить. Нам это не нужно, оставлено для примера
    const localStorageReaction = reaction(
        () => toJS(app.store), // Функция-селектор данных
        saveState // Функция, которая будет вызвана при изменении данных, которые возвращает селектор
    );

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

Fonksyon reyaksyon an enteresan isit la. Li gen de agiman:

  1. Done seleksyon an.
  2. Yon moun kap okipe yo pral rele ak done sa yo chak fwa li chanje.

Kontrèman ak redux, kote nou resevwa eta a klèman kòm yon agiman, mobx sonje ki obsèvab nou jwenn aksè andedan seleksyon an, epi sèlman rele moun kap okipe a lè yo chanje.

Li enpòtan pou w konprann egzakteman ki jan mobx deside nan ki observab nou abònman. Si mwen te ekri yon seleksyon nan kòd tankou sa a() => app.store, Lè sa a, reyaksyon yo pa janm pral rele, depi depo nan tèt li se pa obsèvab, se sèlman jaden li yo.

Si mwen te ekri l konsa () => app.store.keys, Lè sa a, ankò pa gen anyen ta rive, depi lè ajoute / retire eleman etalaj, referans a li pa pral chanje.

Mobx aji kòm yon seleksyon pou premye fwa epi sèlman kenbe tras de observables ke nou te jwenn aksè. Sa a se fè atravè getters proxy. Se poutèt sa, se fonksyon an entegre yo itilize isit la toJS. Li retounen yon nouvo objè ak tout proxy ranplase ak jaden orijinal yo. Pandan ekzekisyon, li li tout jaden yo nan objè a - kidonk getters yo deklanche.

Nan konsole popup la nou pral ajoute plizyè kle ankò. Fwa sa a, yo tou te fini nan localStorage:

Ekri yon ekstansyon navigatè sekirite

Lè yo rechaje paj background nan, enfòmasyon an rete an plas.

Tout kòd aplikasyon jiska pwen sa a ka wè isit la.

Sekirize depo kle prive

Sere kle prive yo nan tèks klè pa an sekirite: toujou gen yon chans pou yo rache ou, jwenn aksè nan òdinatè w lan, ak sou sa. Se poutèt sa, nan localStorage nou pral estoke kle yo nan yon fòm modpas-chiffre.

Pou pi gwo sekirite, nou pral ajoute yon eta fèmen nan aplikasyon an, nan ki pa pral gen aksè a kle yo ditou. Nou pral otomatikman transfere ekstansyon an nan eta a fèmen akòz yon delè.

Mobx pèmèt ou sere sèlman yon seri minimòm de done, epi rès la otomatikman kalkile ki baze sou li. Sa yo se sa yo rele pwopriyete yo kalkile. Yo ka konpare ak opinyon nan baz done:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Утилиты для безопасного шифрования строк. Используют crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Храним пароль и зашифрованные ключи. Если пароль null - приложение locked
            password: null,
            vault: initState.vault,

            // Геттеры для вычислимых полей. Можно провести аналогию с view в бд.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Инициализация пустого хранилища новым паролем
    @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
        )
    }

    ... // код подключения и api

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

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

    // Методы для шифровки/дешифровки хранилища
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Vault not initialized')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Wrong password')
        }
    }
}

Koulye a, nou sèlman magazen kle yo chiffres ak modpas. Tout lòt bagay yo kalkile. Nou fè transfè a nan yon eta fèmen lè nou retire modpas la nan eta a. API piblik la kounye a gen yon metòd pou inisyalize depo a.

Ekri pou chifreman sèvis piblik lè l sèvi avèk crypto-js:

import CryptoJS from 'crypto-js'

// Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей
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)
}

Navigatè a gen yon API san fè anyen konsa kote ou ka abònman nan yon evènman - chanjman eta. Eta, kòmsadwa, pouvwa dwe idle, active и locked. Pou san fè anyen konsa, ou ka mete yon delè, ak fèmen se mete lè eksplwatasyon an tèt li bloke. Nou pral tou chanje seleksyon an pou ekonomize nan localStorage:

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

    // Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Таймаут бездействия, когда сработает событие
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Connect to other contexts
    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)
        }
    }
}

Kòd la anvan etap sa a se isit la.

Tranzaksyon yo

Se konsa, nou rive nan bagay ki pi enpòtan: kreye ak siyen tranzaksyon sou blockchain la. Nou pral sèvi ak blòk ak bibliyotèk WAVES la vag-tranzaksyon.

Premyèman, ann ajoute nan eta a yon seri mesaj ki bezwen siyen, epi ajoute metòd pou ajoute yon nouvo mesaj, konfime siyati a, epi refize:

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) {
        // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд.
        const message = observable.object({
            id: uuid(), // Идентификатор, используюю uuid
            origin, // Origin будем впоследствии показывать в интерфейсе
            data, //
            status: 'new', // Статусов будет четыре: new, signed, rejected и failed
            timestamp: Date.now()
        });
        console.log(`new message: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, //Будем обсервить статус сообщеня
                (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('User rejected message'));
                            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(`No msg with 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(`No msg with id:${id}`);
        message.status = 'rejected'
    }

    ...
}

Lè nou resevwa yon nouvo mesaj, nou ajoute metadata pou li, fè observable epi ajoute nan store.messages.

Si ou pa fè sa observable manyèlman, Lè sa a, mobx pral fè li tèt li lè ajoute mesaj nan etalaj la. Sepandan, li pral kreye yon nouvo objè ke nou pa pral gen yon referans, men nou pral bezwen li pou pwochen etap la.

Apre sa, nou retounen yon pwomès ki rezoud lè estati mesaj la chanje. Estati a kontwole pa reyaksyon, ki pral "touye tèt li" lè estati a chanje.

Kòd metòd approve и reject trè senp: nou tou senpleman chanje estati a nan mesaj la, apre yo fin siyen li si sa nesesè.

Nou mete Apwouve ak rejte nan API UI, newMessage nan API paj la:

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

    ...
}

Koulye a, ann eseye siyen tranzaksyon an ak ekstansyon an:

Ekri yon ekstansyon navigatè sekirite

An jeneral, tout bagay pare, tout sa ki rete se ajoute senp UI.

UI

Koòdone a bezwen aksè nan eta aplikasyon an. Sou bò UI nou pral fè observable eta epi ajoute yon fonksyon nan API a ki pral chanje eta sa a. Ann ajoute observable nan objè a API resevwa nan 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)
}

Nan fen a nou kòmanse rann koòdone aplikasyon an. Sa a se yon aplikasyon reyaji. Se objè a background tou senpleman pase lè l sèvi avèk akseswar. Li ta kòrèk, nan kou, fè yon sèvis separe pou metòd ak yon magazen pou eta a, men pou rezon atik sa a sa a se ase:

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

// Инициализируем приложение с background объектом в качест ве props
export async function initApp(background){
    render(
        <App background={background}/>,
        document.getElementById('app-content')
    );
}

Avèk mobx li trè fasil pou kòmanse rann lè done yo chanje. Nou tou senpleman pann dekoratè obsèvatè a nan pake a mobx-reaji sou eleman an, ak rann yo pral otomatikman rele lè nenpòt obsèvab referans pa eleman nan chanje. Ou pa bezwen okenn mapStateToProps oswa konekte tankou nan redux. Tout bagay ap travay soti nan bwat la:

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 // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {

    // Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
    // и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
    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 > 0
                        ?
                        <Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
                        :
                        <Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
            }
            <div>
                {!locked && <button onClick={() => lock()}>Lock App</button>}
                {initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
            </div>
        </Fragment>
    }
}

Konpozan ki rete yo ka wè nan kòd la nan katab la UI.

Koulye a, nan klas aplikasyon an ou bezwen fè yon seleksyon eta pou UI a epi notifye UI a lè li chanje. Pou fè sa, ann ajoute yon metòd getState и reactionrele 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 {

    ...

    // public
    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) => {
            // Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу.
                // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce
                {fireImmediately: true, delay: 500}
            );
            // Удалим подписку при отключении клиента
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Lè w ap resevwa yon objè remote se kreye reaction chanje eta a ki rele fonksyon an sou bò UI.

Touch final la se ajoute ekspozisyon nouvo mesaj sou icon ekstansyon an:

function setupApp() {
...

    // Reaction на выставление текста беджа.
    reaction(
        () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
        text => extensionApi.browserAction.setBadgeText({text}),
        {fireImmediately: true}
    );

...
}

Se konsa, aplikasyon an pare. Paj Web yo ka mande yon siyati pou tranzaksyon yo:

Ekri yon ekstansyon navigatè sekirite

Ekri yon ekstansyon navigatè sekirite

Kòd la disponib isit la lyen.

Konklizyon

Si ou te li atik la jiska la fen, men ou toujou gen kesyon, ou ka poze yo nan depo ak ekstansyon. La ou pral jwenn tou komite pou chak etap deziyen.

Men, si ou enterese nan gade nan kòd la pou ekstansyon aktyèl la, ou ka jwenn sa a isit la.

Kòd, depo ak deskripsyon travay soti nan siemarell

Sous: www.habr.com

Add nouvo kòmantè