Invisible Keys Snap: Multi-Cloud-Speicherung privater Schlüssel

Ein ETHLisbon Hackathon-Gewinner

8 Min. Lesezeit
Invisible Keys Snap: Multi-Cloud-Speicherung privater Schlüssel

MetaMask Snaps ist der Fahrplan, um MetaMask zum erweiterbarsten Wallet der Welt zu machen. Als Entwickler kannst du MetaMask auf völlig neue Weise mit deinen Features und APIs bereichern. Web3-Entwickler sind das Herzstück dieses Wachstums, und diese Serie soll die innovativen MetaMask Snaps vorstellen, die heute entwickelt werden.

Invisible Keys Snap

Warum habt ihr es gebaut?

Extern verwaltete Konten (Externally-owned accounts) bilden das Fundament nahezu jeder dApp oder jeden Dienstes im Ethereum-Ökosystem. Es ist jedoch alles andere als trivial, eine Wallet-Anwendung zu entwickeln, die sowohl sicher als auch benutzerfreundlich ist. Um dem Nutzer die volle Kontrolle und ein hohes Maß an Sicherheit zu bieten, greifen wir typischerweise auf komplexe und unpraktische Mechanismen zurück. Eine unkomplizierte und angenehme Nutzererfahrung hingegen lässt sich meist nur erreichen, indem Nutzer die Kontrolle über ihre Schlüssel abgeben.

Mit InvisibleKeys haben wir auf MetaMasks renommierter Nutzererfahrung aufgebaut, um einen neuen Kompromiss zwischen Benutzerfreundlichkeit und Sicherheit vorzuschlagen – und MetaMask damit die Verbindung zu einem innovativen externen Multi-Cloud-Wallet zu ermöglichen.

Wie? Das Multi-Cloud-Wallet von InvisibleKeys speichert die privaten Schlüssel des Nutzers in zwei oder mehr Cloud-Speicherdiensten (z. B. Google Drive, Dropbox usw.) – und zwar so, dass die Schlüssel selbst dann nicht kompromittiert werden, wenn einer dieser Dienste betroffen ist. Dieser Multi-Cloud-Ansatz folgt einem patentierten Verfahren (Patent US20190095628), das ein sehr hohes Sicherheitsniveau gewährleistet und gleichzeitig die Synchronisierung über mehrere Geräte ermöglicht.

Um ein Konto aus der Cloud zu importieren, liest MetaMask eine JSON-Datei in deinem Google Drive (oder einem anderen Speicherdienst) mit einem Array deiner öffentlichen Schlüssel. Um eine Transaktion zu signieren, wird dein privater Schlüssel (PK) vorübergehend und lokal in einer externen Web-App rekonstruiert, die MetaMasks Signaturanfragen entgegennimmt, die Transaktion signiert und die signierte Transaktion an MetaMask zurückgibt. Die Schlüssel werden nie dauerhaft gespeichert und existieren nur vorübergehend im Arbeitsspeicher. Um die volle Nutzerkontrolle zu wahren, wird auf den PK niemals durch die MetaMask-Erweiterung zugegriffen. Selbst wenn das Wallet entsperrt oder kompromittiert ist, kann der PK nicht gestohlen werden.

InvisibleKeys ermöglicht es Nutzern, von überall auf der Welt auf ihr Wallet zuzugreifen, da alles in der Cloud gespeichert ist. Dies ergänzt den traditionellen MetaMask-Ansatz und wahrt MetaMasks hohe Standards für Benutzerfreundlichkeit und UI-Design.

Architektur

InvisibleKeys arbeitet mit zwei Hauptkomponenten: dem MetaMask-Wallet und der externen Web-App.

Da wir die Transaktion außerhalb des Wallets signieren wollen, haben wir den Teil des Codes genutzt, der für die Verbindung und Signierung mit Hardware-Wallets zuständig ist. Anstatt die zu signierende Transaktion an eine bestimmte Hardware zu senden, wird ein Popup mit unserer Web-App geöffnet, in der die Signierung durchgeführt wird.

In der Web-App authentifiziert sich der Nutzer mit seinen Cloud-Zugangsdaten, der private Schlüssel wird abgerufen, die Transaktion wird signiert und der Schlüssel wird aus dem Arbeitsspeicher gelöscht. Anschließend wird das Popup geschlossen und der Nutzer kehrt zum MetaMask-Wallet zurück, das sich in einem Wartezustand befand und auf eine signierte Transaktion gewartet hat.

Die beiden Komponenten kommunizieren über die window Web API des Browsers. Dies ist in diesem Kontext ausreichend, da zwischen den beiden Komponenten zu keinem Zeitpunkt private Informationen ausgetauscht werden.

UI-Änderungen

Zunächst mussten wir einige UI-Änderungen an MetaMask vornehmen, um unsere InvisibleKeys-Verbindungsstrategie hinzuzufügen. Sie wird auf dieselbe Weise hinzugefügt, wie ein neues unterstütztes Hardware-Wallet zur App hinzugefügt werden würde.

Dies sind einige der am Code vorgenommenen Änderungen: Ein neues Gerät und ein neuer Typ wurden hinzugefügt sowie der neue Button für die UI.

app/scripts/metamask-controller.js

keyringName = InvisibleKeyring.type;
break;
Kopieren

ui/pages/create-account/connect-hardware/select-hardware.js

  renderConnectToInvisibleButton() {
    return (
      <button
        className={classnames('hw-connect__btn', {
          selected: this.state.selectedDevice === 'invisible',
        })}
        onClick={(_) => this.setState({ selectedDevice: 'invisible' })}
      >
        <img
          className="hw-connect__btn__img"
          src="images/invisible-logo.png"
          alt="Invisible"
        />
      </button>
    );
  }
Kopieren

Invisible Keys Keyring

Um verschiedene Hardware-Wallets zu verbinden, fungiert die Keyring-Klasse als Schnittstelle zur Kommunikation zwischen dem Wallet und dem Gerät der jeweiligen Marke. Die wichtigsten Methoden zum Importieren von Konten und Signieren von Transaktionen sind: addAccounts(), die ein Array von Adressen zurückgibt, und signTransaction(), die eine signierte Transaktion zurückgibt.

Wir haben diese Methoden in InvisibleKeyring implementiert. Die erste öffnet das Popup und erwartet eine Nachricht mit einem Array von Adressen. Die zweite öffnet das Popup mit der unsignierten Transaktion, die in einer Nachricht kodiert ist. Anschließend erwartet sie eine kodierte signierte Transaktion, die zurückgegeben und vom Hauptcontroller gesendet wird. Während auf das Schließen des Popups gewartet wird, befindet sich MetaMask in einem Ladezustand.

Diese Methoden rufen eine andere Klasse auf, InvisibleConnect, die für die Verbindung und Kommunikation mit der Web-App zuständig ist.

./InvisibleConnect.js

export default class InvisibleConnect {

  {...}

  const appUrl = 'http://url-to-web-app';

  async getAccountsCloud() {
    let newAccounts = []

    //create a listener for the imported accounts
    window.addEventListener(
      'message',
      event => {
        if (event.data.event_id === 'imported_accounts') {
          newAccounts = event.data.accounts
        }
      },
      false,
    )

    return new Promise(async resolve => {
      const child = window.open(appUrl + '/import')
      const interval = setInterval(() => {
        if (child.closed) {
          //when the popup is closed
          clearInterval(interval)
          this.accounts = newAccounts
          resolve(newAccounts)
        }
      }, 1000)
    })
  }

  async signTxCloud(transaction) {
    let signedTx = {}

    //create a listener for the signedTx
    window.addEventListener(
      'message',
      event => {
        if (event.data.event_id === 'signedTx') {
          signedTx = {
            v: event.data.v,
            r: event.data.r,
            s: event.data.s,
          }
        }
      },
      false,
    )

    return new Promise(async resolve => {
      const child = window.open(appUrl + '/sign') //open popup

      child.postMessage(
        //sends message to the popup
        {
          event_id: 'unsignedTx',
          data: {
            tx: transaction.serialize().toString('hex'),
          },
        },
        '*',
      )

      const interval = setInterval(() => {
        if (child.closed) {
          //when the popup is closed
          clearInterval(interval)
          resolve(signedTx)
        }
      }, 1000)
    })
  }
}
./eth-invisible-keyring.js

import InvisibleConnect from './InvisibleConnect'

class InvisibleKeyring extends EventEmitter {

  {...}

  addAccounts(n = 1) {
    return new Promise((resolve, reject) => {
      this.unlock()
        .then(async _ => {
          const from = this.unlockedAccount
          const to = from + n
          this.accounts = []

          for (let i = from; i < to; i++) {
            const address = InvisibleConnect.getAccounts(i).address

            this.accounts.push(address)
            this.accountIndexes[ethUtil.toChecksumAddress(address)] = i
            this.page = 0
          }
          resolve(this.accounts) //resolve the new accounts
        })
        .catch(e => {
          reject(e)
        })
    })
  }

  signTransaction(address, tx) {
    return new Promise(async (resolve, reject) => {
      try {
        const signedTx = await InvisibleConnect.signTxCloud(tx)
        const txData = tx.toJSON()
        txData.v = ethUtil.addHexPrefix(signedTx.v)
        txData.r = ethUtil.addHexPrefix(signedTx.r)
        txData.s = ethUtil.addHexPrefix(signedTx.s)

        const common = tx.common
        const freeze = Object.isFrozen(tx)
        const feeMarketTransaction = FeeMarketEIP1559Transaction.fromTxData(
          txData,
          {common, freeze},
        )
        resolve(feeMarketTransaction)
      } catch (err) {
        reject(new Error(err))
      }
    })
  }

   {...}
}
Kopieren

Externe App

Die App hat zwei Seiten: eine, auf der die Konten importiert werden, und eine, auf der die Transaktion signiert wird.

Auf der ersten Seite authentifiziert sich der Nutzer bei Google Drive, wo eine Datei mit seinen öffentlichen Schlüsseln unter metamask/public.json gespeichert ist. Nach dem Klicken auf den Import-Button wird die Datei abgerufen, die Schlüssel werden ausgelesen und über die window Web API an das MetaMask-Wallet gesendet.

const sendAccountsToMetamask = (publicKeys: Address[]) => {
	window.opener.postMessage(
	  {
	    event_id: 'importAccounts',
	    data: {
	      accounts: publicKeys,
	    },
	  },
	  '*'
	);
};

const getPublicKeys = async (): void => {
     const publicKeys: Address[] = await getGoogleDrivePublicKeys();
     sendAccountsToMetamask(publicKeys);
     return;
};
Kopieren

Auf der zweiten Seite muss sich der Nutzer sowohl bei Google Drive als auch bei Dropbox authentifizieren – die Dienste, die wir für dieses Beispiel verwendet haben. Der Ansatz ist mit jedem Speicherdienst kompatibel. Voraussetzung ist, dass es sich um unabhängige Dienste handelt, die nicht miteinander kooperieren. Nach der Authentifizierung werden die verschiedenen Informationsteile aus diesen Diensten abgerufen und der private Schlüssel mithilfe der decrypt()-Methode rekonstruiert, die gemäß dem im oben genannten Patent beschriebenen Verfahren implementiert ist, und die Transaktion wird signiert. Unmittelbar danach wird der private Schlüssel aus dem Arbeitsspeicher gelöscht. Abschließend wird die signierte Transaktion an das MetaMask-Wallet zurückgesendet.

Hier ist der Code Schritt für Schritt. Es werden nur die relevanten Teile des Codes gezeigt. Alles, was mit dem UI-Zustand oder der API-Kommunikation zusammenhängt, wurde weggelassen.

const sendTxToMetamask = (v, r, s) => {
	window.opener.postMessage(
	  {
	    event_id: 'signedTx',
	    data: {
	      v: v,
	      r: r,
	      s: s,
	    },
	  },
	  '*'
	);
};

const signTransaction = async (): void => {
     const trs = TransactionFactory.fromSerializedData(Buffer.from(tx, 'hex'));
     const googleDrivePart = await getGoogleDrivePart();
     const dropboxPart = await getDropboxPart();

     const piecesArray = [
         Buffer.from(googleDrivePart, 'hex'), 
         Buffer.from(dropboxPart, 'hex')
     ];
     const decrypted = decrypt({ data: piecesArray });
     let plainTextPK = Buffer.from(
         new TextDecoder().decode(decrypted.data),
         'hex'
     );

     if (!ethUtil.isValidPrivate(plainTextPK)) {
        alert('Failed to decrypt Private Key');
        return;
     }

     const signedTx = trs.sign(plainTextPK);
     plainTextPK = null; //erase from memory

     const v = ethUtil.stripHexPrefix(ethUtil.bufferToHex(signed_tx.v));
     const r = ethUtil.stripHexPrefix(ethUtil.bufferToHex(signed_tx.r));
     const s = ethUtil.stripHexPrefix(ethUtil.bufferToHex(signed_tx.s));

     if (signed_tx.verifySignature()) {
         sendTxToMetamask(v, r, s);
     } else {
         alert('Signature failed');
     }
    return;
}
Kopieren

Könnt ihr uns ein wenig über euch und euer Team erzählen?

Wir sind ein begeistertes Team von Software-Ingenieuren mit Sitz in Braga, Portugal, und einem starken Hintergrund in verteilten Systemen. Wir haben Invisible Lab gegründet, ein Startup, das sich auf die Lösung komplexer Probleme im Blockchain- und Web3-Bereich konzentriert.

Wann seid ihr zum ersten Mal mit MetaMask Snaps in Berührung gekommen und wie war eure Erfahrung dabei?

Das erste Mal sind wir mit MetaMask Snaps beim Launch des SDK in Berührung gekommen. Seitdem hat sich die Technologie erheblich weiterentwickelt und tut dies weiterhin.

Was unterscheidet MetaMask Snaps von anderen Wallets?

Das MetaMask-Snaps-Wallet bietet die Möglichkeit, Features darauf aufzubauen, während ein Design beibehalten wird, mit dem Millionen von Nutzern bereits täglich interagieren.

Erzählt uns, wie es für euch und euer Team ist, Snaps mit MetaMask zu entwickeln.

Wir haben eine Vorabversion von MetaMask Snaps verwendet, die noch nicht alle Features unterstützte, die wir für dieses Projekt benötigten. Wir sind jedoch überzeugt, dass alles, was wir gebaut haben, in naher Zukunft mit Snaps auf eine viel einfachere und reibungslosere Weise möglich sein wird.

Was bedeutet MetaMask Snaps für euch?

Es bedeutet Anpassbarkeit, Erweiterbarkeit und eine Fülle von Möglichkeiten, auf Basis des MetaMask-Wallets Innovationen zu entwickeln.

Welche Chancen seht ihr in MetaMask Snaps und den Möglichkeiten, die es für den Web3-Bereich eröffnet?

Eine großartige Chance, die wir sehen und interessant finden, ist die Entwicklung zusätzlicher Sicherheitsstrategien. Dabei könnten verschiedene Sicherheitsstufen je nach Erfahrung und Risikobereitschaft der einzelnen Nutzer geschaffen werden. Dies könnte einen erheblichen Einfluss darauf haben, die Anfälligkeit der Nutzer für Sicherheitsprobleme und technische Angriffe zu reduzieren, die in diesem Bereich leider weit verbreitet sind. Die Eindämmung dieser Probleme ist entscheidend für die Massenadoption von Web3.

Habt ihr Ratschläge für Entwickler, die MetaMask Snaps ausprobieren möchten?

Keine Scheu – einfach loslegen! Du wirst überrascht sein, was du mit MetaMask Snaps alles erreichen kannst. Ob durch die Integration neuer Blockchains, Features oder Sicherheitsansätze!

Mit MetaMask Snaps entwickeln

So startest du mit MetaMask Snaps:

  1. Schau dir die Entwicklerdokumentation an

  2. Installiere MetaMask Flask

  3. Lies den MetaMask-Snaps-Leitfaden

  4. Bleib mit uns in Kontakt auf Twitter, in den GitHub-Diskussionen und auf Discord

Halte bei den nächsten Hackathons in deiner Nähe Ausschau nach unserem Team! Happy BUIDLing ⚒️

Haftungsausschluss: MetaMask Snaps werden in der Regel von Drittanbietern entwickelt, die nicht zu Consensys Software gehören. Die Nutzung von durch Dritte entwickelten MetaMask Snaps erfolgt nach eigenem Ermessen und auf eigenes Risiko, und du stimmst zu, dass du allein für etwaige Verluste oder Schäden verantwortlich bist, die aus solchen Aktivitäten resultieren. Consensys gibt keine ausdrücklichen oder stillschweigenden Garantien, weder mündlich noch schriftlich, in Bezug auf durch Dritte entwickelte MetaMask Snaps und übernimmt keine Haftung für durch Dritte entwickelte MetaMask Snaps. Die Nutzung von Blockchain-bezogener Software ist mit Risiken verbunden, die du bei der Verwendung von MetaMask Snaps vollständig selbst trägst.

KI-übersetzt. Kann Fehler enthalten. Bitte überprüfen Sie die Informationen stets.

Bewerten Sie die Übersetzung
  • MetaMask
    MetaMask

    MetaMask is the world's leading self-custodial crypto wallet and gateway to decentralized finance.

    Alle Artikel lesen