Invisible Keys Snap: 멀티 클라우드 개인 키 저장

ETHLisbon 해커톤 수상작

8분 읽기
Invisible Keys Snap: 멀티 클라우드 개인 키 저장

MetaMask Snaps는 MetaMask를 세계에서 가장 확장 가능한 지갑으로 만들기 위한 로드맵입니다. 개발자라면 완전히 새로운 방식으로 자신만의 기능과 API를 MetaMask에 구현할 수 있습니다. Web3 개발자들은 이러한 성장의 핵심이며, 이 시리즈는 현재 개발 중인 혁신적인 MetaMask Snaps를 소개하는 것을 목표로 합니다.

Invisible Keys Snap

왜 만들었나요?

외부 소유 계정(EOA)은 이더리움 생태계의 거의 모든 dapp 또는 서비스의 핵심입니다. 그러나 안전하면서도 사용하기 쉬운 지갑 애플리케이션을 설계하고 구현하는 것은 결코 간단한 일이 아닙니다. 일반적으로 사용자에게 완전한 제어권과 높은 수준의 보안을 제공하기 위해 복잡하고 비실용적인 메커니즘에 의존하게 됩니다. 반면, 직관적이고 쾌적한 사용자 경험을 제공하려면 사용자가 키에 대한 제어권을 포기하도록 요구하는 방식을 택하게 됩니다.

InvisibleKeys를 통해 저희는 MetaMask의 뛰어난 사용자 경험을 기반으로, 사용자 경험과 보안 사이의 새로운 균형점을 제시하고 MetaMask를 혁신적인 외부 멀티 클라우드 지갑과 연결할 수 있도록 했습니다.

어떻게 작동하나요? InvisibleKeys의 멀티 클라우드 지갑은 사용자의 개인 키를 두 개 이상의 클라우드 스토리지 서비스(예: Google Drive, Dropbox 등)에 분산 저장하여, 그 중 하나의 서비스가 침해되더라도 키가 절대 노출되지 않도록 합니다. 이 멀티 클라우드 방식은 특허 등록된 워크플로우(특허 US20190095628)를 따르며, 매우 높은 수준의 보안을 보장하는 동시에 다중 기기 동기화를 지원합니다.

클라우드에서 계정을 가져오려면 MetaMask가 Google Drive(또는 다른 스토리지 서비스)에서 공개 키 배열이 담긴 JSON 파일을 읽습니다. 그런 다음 트랜잭션에 서명하기 위해 개인 키(PK)가 외부 웹 앱에서 일시적으로 로컬에서 재구성됩니다. 이 웹 앱은 MetaMask의 서명 요청을 수신하고, 트랜잭션에 서명한 후 서명된 트랜잭션을 MetaMask로 반환합니다. 키는 절대 저장되지 않으며 메모리에만 일시적으로 존재합니다. 또한 완전한 사용자 제어권을 유지하기 위해 PK는 MetaMask 확장 프로그램에서 절대 접근할 수 없습니다. 지갑이 잠금 해제되거나 침해되더라도 PK는 탈취될 수 없습니다.

InvisibleKeys를 사용하면 모든 것이 클라우드에 저장되어 있어 전 세계 어디서든 지갑에 접근할 수 있으며, 기존 MetaMask 방식을 보완하면서 MetaMask의 높은 사용성 및 UI 인터페이스 기준을 유지합니다.

아키텍처

InvisibleKeys는 MetaMask 지갑과 외부 웹 앱, 두 가지 주요 구성 요소로 작동합니다.

지갑 외부에서 트랜잭션에 서명하고자 했기 때문에, 저희는 하드웨어 지갑 연결/서명 코드 부분을 활용했습니다. 트랜잭션을 특정 하드웨어로 전송하는 대신, 서명이 실행될 웹 앱이 담긴 팝업이 열립니다.

웹 앱에서 사용자는 클라우드 자격 증명으로 인증하고, 개인 키를 가져오고, 트랜잭션에 서명한 후 키가 메모리에서 삭제됩니다. 마지막으로 팝업이 닫히고 사용자는 서명된 트랜잭션 전송을 대기 중이던 MetaMask 지갑으로 돌아옵니다.

두 구성 요소는 브라우저의 window web API를 통해 통신합니다. 두 구성 요소 간에 개인 정보가 교환되지 않으므로 이 방식으로 충분합니다.

UI 변경 사항

먼저 InvisibleKeys 연결 전략을 추가하기 위해 MetaMask의 UI를 일부 변경해야 했습니다. 새로운 지원 하드웨어 지갑을 앱에 추가하는 것과 동일한 방식으로 추가됩니다.

다음은 코드에 적용된 변경 사항 중 일부입니다. 새로운 기기와 타입, 그리고 UI용 새 버튼이 추가되었습니다.

app/scripts/metamask-controller.js

keyringName = InvisibleKeyring.type;
break;
복사

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>
    );
  }
복사

Invisible Keys Keyring

다양한 하드웨어 지갑을 연결하기 위해 Keyring 클래스는 지갑과 기기 브랜드 간의 통신 인터페이스 역할을 합니다. 계정 가져오기 및 트랜잭션 서명에 필요한 주요 메서드는 주소 배열을 반환하는 addAccounts()와 서명된 트랜잭션을 반환하는 signTransaction()입니다.

저희는 InvisibleKeyring에 이 메서드들을 구현했습니다. 첫 번째 메서드는 팝업을 열고 주소 배열이 담긴 메시지를 기다립니다. 두 번째 메서드는 메시지에 인코딩된 미서명 트랜잭션과 함께 팝업을 열고, 인코딩된 서명 트랜잭션을 기다린 후 이를 반환하면 메인 컨트롤러가 전송합니다. 팝업이 닫힐 때까지 MetaMask는 로딩 상태를 유지합니다.

이 메서드들은 웹 앱 연결 및 통신을 담당하는 별도의 클래스 InvisibleConnect를 호출합니다.

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

   {...}
}
복사

외부 앱

이 앱은 두 개의 페이지로 구성됩니다. 하나는 계정을 가져오는 페이지이고, 다른 하나는 트랜잭션에 서명하는 페이지입니다.

첫 번째 페이지에서 사용자는 metamask/public.json에 공개 키 파일이 저장된 Google Drive에 인증합니다. 그런 다음 가져오기 버튼을 클릭하면 파일을 가져와 키를 읽고 window web API를 통해 MetaMask 지갑으로 전송합니다.

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

두 번째 페이지에서 사용자는 이 예시에서 사용한 서비스인 Google Drive와 Dropbox 모두에 인증해야 합니다. 이 방식은 모든 스토리지 서비스와 호환됩니다. 단, 서비스들이 서로 공모하지 않는 독립적인 서비스여야 합니다. 인증 후 해당 서비스에서 각각의 정보를 가져오고, 앞서 언급한 특허에 설명된 메커니즘에 따라 구현된 decrypt() 메서드를 사용하여 개인 키를 재구성하고 트랜잭션에 서명합니다. 서명 직후 개인 키는 메모리에서 삭제됩니다. 마지막으로 서명된 트랜잭션이 MetaMask 지갑으로 다시 전송됩니다.

다음은 단계별 코드입니다. 코드의 관련 부분만 표시되며, UI 상태 또는 API 통신과 관련된 모든 내용은 생략되었습니다.

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

본인과 팀에 대해 간단히 소개해 주시겠어요?

저희는 포르투갈 브라가에 기반을 둔 열정적인 소프트웨어 엔지니어 팀으로, 분산 시스템에 대한 탄탄한 배경 지식을 보유하고 있습니다. 블록체인과 Web3 분야의 어려운 문제를 해결하는 데 집중하는 스타트업 Invisible Lab을 설립했습니다.

MetaMask Snaps를 처음 접한 것은 언제이며, 어떤 경험이었나요?

MetaMask Snaps를 처음 접한 것은 SDK 출시 때였습니다. 그 이후로 기술이 크게 발전했으며 지금도 계속 발전하고 있습니다.

MetaMask Snaps가 다른 지갑과 다른 점은 무엇인가요?

MetaMask Snaps 지갑은 수백만 명의 사용자가 매일 사용하는 디자인을 유지하면서 그 위에 기능을 구축할 수 있는 능력을 제공합니다.

MetaMask로 Snaps를 개발하는 경험은 어떤가요?

저희는 이 프로젝트에 필요한 모든 기능을 지원하지 않는 MetaMask Snaps의 초기 버전을 사용했습니다. 하지만 저희가 구축한 모든 것이 가까운 미래에 훨씬 더 간단하고 원활한 방식으로 Snaps를 통해 구현 가능해질 것이라고 믿습니다.

MetaMask Snaps는 여러분에게 어떤 의미인가요?

MetaMask 지갑을 기반으로 혁신을 구축할 수 있는 커스터마이징, 업그레이드 가능성, 그리고 무한한 가능성을 의미합니다.

MetaMask Snaps가 Web3 공간에서 열어주는 기회에 대해 어떻게 생각하시나요?

저희가 흥미롭게 생각하는 놀라운 기회 중 하나는 추가적인 보안 전략을 구축하는 것입니다. 각 사용자가 원하는 경험과 위험 수준에 따라 다양한 보안 수준을 만드는 것입니다. 이는 안타깝게도 이 분야에서 흔히 발생해 온 보안 문제와 기술 해킹에 대한 사용자 노출을 줄이는 데 상당한 영향을 미칠 수 있습니다. 이러한 문제를 완화하는 것은 Web3의 대중화를 위해 매우 중요합니다.

MetaMask Snaps를 시도하려는 개발자들에게 조언이 있다면?

망설이지 말고 뛰어드세요! MetaMask Snaps로 무엇을 이룰 수 있는지 놀라게 될 것입니다. 새로운 블록체인 통합이든, 새로운 기능이든, 새로운 보안 접근 방식이든 모두 가능합니다!

MetaMask Snaps로 개발 시작하기

MetaMask Snaps를 시작하려면:

  1. 개발자 문서를 확인하세요

  2. MetaMask Flask를 설치하세요

  3. MetaMask Snaps 가이드를 참고하세요

  4. Twitter, GitHub discussions, Discord에서 저희와 소통하세요

다음 해커톤에서 저희 팀을 만나보세요! 즐거운 BUIDLing ⚒️

면책 조항: MetaMask Snaps는 일반적으로 Consensys Software 이외의 제3자가 개발합니다. 제3자가 개발한 MetaMask Snaps의 사용은 전적으로 사용자 본인의 판단과 위험 부담 하에 이루어지며, 그로 인한 손실이나 피해에 대한 책임은 전적으로 사용자에게 있습니다. Consensys는 제3자가 개발한 MetaMask Snaps에 대해 구두 또는 서면을 불문하고 어떠한 명시적 또는 묵시적 보증도 하지 않으며, 제3자가 개발한 MetaMask Snaps에 대한 모든 책임을 부인합니다. 블록체인 관련 소프트웨어 사용에는 위험이 따르며, MetaMask Snaps를 사용함으로써 해당 위험을 전적으로 감수하게 됩니다.

AI가 번역했습니다. 오류가 있을 수 있습니다. 항상 정보를 확인하시기 바랍니다.

번역 평가하기
  • MetaMask
    MetaMask

    Consensys가 구축한 선도적인 자기 보관형 암호화폐 지갑이자 Web3로 가는 관문입니다.

    모든 글 읽기