この記事は、株式会社うるる(ULURU) Advent Calendar 2023の18日目の記事です。
はじめに
最近、Web3.Storageというサービスに触れる機会があったので、使い方やできることをまとめつつ、暗号化技術を組み合わせてファイルを安全に管理する方法を模索します。
この記事で登場するコード群
以下Githubレポジトリにて公開しています。
概要
本記事で登場するWeb3.Storage, Filecoinストレージについて説明します。
詳細まで踏み込むと長くなってしまうので、概要の説明に留めています。
より詳しくは、添付している公式のリンクなどを参照ください。
Web3.Storage とは
Web3.Storageとは、Filecoinストレージへのアクセスを容易 & シンプルにしてくれるインタフェースサービスのことです。
開発者が裏側の仕組みを意識することなく、Filecoinストレージに対してファイルのアップロード & ダウンロードなどを行うことができます。
Filecoinストレージとは
Filecoinとは、近年登場した分散型ストレージのことです。
大きな特徴として、全世界に存在するデジタルデバイスの空き容量をストレージとして利用する ということがあります。
世界中の空きデバイスにファイルを分散させる仕組みは、IPFSというP2P通信のプロトコルを用いて実現されています。
Filecoinでは、自分の持つ空きデバイス容量の一部を貸し出し、報酬を得るような経済モデルを構築したことで、空きストレージが共有され続けるような仕組みになっています。
イメージとしては、Airbnbのファイル版 といった感じです。
Web3.Storageを使ってみる
最近、大きなアップデートが入ったそうで、過去に自分が触った時点と仕様が結構変わっていました・・・
本記事ではアップデート後のWeb3.Storageで動かしてみたいので、まずはアカウント設定から、ファイルのアップロード・ダウンロードなど基本操作を試してみます。
アカウント & スペース作成
公式サイトの案内に沿って、アカウント & スペースを作成します。
UI上は、スペースに対して、ファイルをアップロード & 管理するような動きになります。
また、各スペースにはDID
という一意なIDが割り振られており、プログラムからアクセスする際などはこの値を使うことになります。
ファイルをアップロードしてみる
JavaScript クライアントが提供されているので、これを使ってみます。
まずは、プログラムからアクセス可能にするための、認証用コードが必要になります。
import { create } from '@web3-storage/w3up-client';
import dotenv from 'dotenv';
dotenv.config();
const client = await create();
const account = await client.login(process.env.LOGIN_ADDRESS);
const did = process.env.DID;
await account.provision(did);
GUI上で、メールアドレスにてログイン & 対象スペースを選択 という動きをプログラムで実施しているイメージです。
DID
は、スペース作成時に表示される値を使いましょう。
試してないですが、毎回ログインするのではなく、「代表者の権限を委任する」ということもできるみたいです。参考
認証が通過したら、ファイルをアップロードするコードを記述します。
import { create } from '@web3-storage/w3up-client';
import { filesFromPaths } from 'files-from-path';
import dotenv from 'dotenv';
dotenv.config();
const client = await create();
// ... 認証コード
const file = await filesFromPaths(['sample_images/sample-image.png']);
const cid = await client.uploadFile(file[0]);
console.log('cid', cid);
コードの全体像はこんな感じ。
サンプルコード
import { create } from '@web3-storage/w3up-client';
import { filesFromPaths } from 'files-from-path';
import dotenv from 'dotenv';
dotenv.config();
async function upload()
{
const client = await getClient();
const file = await filesFromPaths(['sample_images/sample-image.png']);
const cid = await client.uploadFile(file[0]);
console.log('cid', cid);
}
async function getClient()
{
const client = await create();
const account = await client.login(process.env.LOGIN_ADDRESS);
const did = process.env.DID;
await account.provision(did);
await client.setCurrentSpace(did);
return client;
}
await upload()
.catch(console.error)
.finally(() => process.exit())
動かしてみると、Web3.Storageのスペース上にファイルがアップロードされていることが確認できるかと思います。
❯❯❯ make upload
node js/upload.js
cid CID(bafkreiciko4vlpseyuzwhz5h3t5mv5t4ejwrri35vkqpdvtgb654ux7pei)
CIDというのは、アップロードされたファイルのコンテンツ識別子のことです。
ファイルをブラウザ上で表示 & ダウンロードしてみる
URL箇所に表示されている、IPFSゲートウェイURLを叩くことで、ブラウザ上でファイルを表示することができます。
ダウンロードについては、JSクライアントでは今時点で実装が提供されてなさそうだったので、curlコマンドなどで実施することになりそうです。
Web3.Storageと暗号化技術を組み合わせる
ここからが本題です。
Web3.Storageと暗号化技術を組み合わせることで、安全にファイルを保存する方法を模索してみます。
公式ドキュメントでも注意喚起されていますが、アップロードしたファイルについては、CID(コンテンツ識別子)を知っている人であれば誰でもアクセス可能です。
そのため、例えば機密情報を保存したい場合、閲覧可能者をコントロールする工夫が必要になります。
Public Data 🌎
All data uploaded to w3up is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using w3up.
そこで今回は、PGP(Pretty Good Privacy)の仕組みを用いて、ファイルを暗号化した状態でWeb3.Storageにアップロード & 秘密鍵を保有しているユーザのみファイルを復元して閲覧可能 という動きを実装してみます。
鍵の準備
まずは、暗号化に使う公開鍵 & 秘密鍵を作成 & importします。
鍵自体は、gpgコマンドで作成しても、JavaScriptのopenpgp
パケージで作成しても構いません。
JavaScriptで実施するなら以下の感じです。
サンプルコード
import * as openpgp from 'openpgp';
import fs from 'fs';
const { privateKey, publicKey } = await openpgp.generateKey({
type: 'ecc',
curve: 'curve25519',
userIDs: [{
name: process.env.GPG_USER_NAME,
email: process.env.GPG_USER_EMAIL
}],
format: 'armored'
});
console.log(privateKey);
console.log(publicKey);
fs.writeFileSync('private-key.key', privateKey);
鍵作成後は、公開鍵情報はpublic_keys/publicKey1.asc
に記載します。
秘密鍵情報は、復号操作を可能にしたいPC上でgpg --import
コマンドを実施してimportしましょう。
ファイルを暗号化してアップロード
ファイルの暗号化にも、JavaScriptのopenpgp
パケージを使います。
暗号化箇所のコードは以下です。
import openpgp from 'openpgp';
import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config();
// Public keys
const publicKeysArmored = [
fs.readFileSync(process.env.PUBLIC_KEY1_PATH, 'utf8'),
fs.readFileSync(process.env.PUBLIC_KEY2_PATH, 'utf8'),
];
const publicKeys = await Promise.all(
publicKeysArmored.map((armoredKey) => openpgp.readKey({ armoredKey }))
);
// File data
const fileData = fs.readFileSync('sample_images/sample-image.png');
const fileMessage = await openpgp.createMessage({ binary: fileData });
const encrypted = await openpgp.encrypt({
message: fileMessage,
encryptionKeys: publicKeys,
format: "binary",
});
publicKeysArmored
箇所で複数の公開鍵を指定することで、他のユーザが暗号化したファイルについても、自分の保有する秘密鍵を用いて復号することができます。
このようにすることで、「同一グループ間でのみファイルの閲覧可能」といった、グループ認証の機構が実現可能になります。
コードの全体像は以下のようになります。
サンプルコード
import { create } from '@web3-storage/w3up-client';
import { filesFromPaths } from 'files-from-path';
import openpgp from 'openpgp';
import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config();
async function upload()
{
const client = await getClient();
// encrypt file
const encrypted = await encryptFile();
const tempFilePath = 'tmp-sample-image.png';
fs.writeFileSync(tempFilePath, new Uint8Array(encrypted));
// upload encrypted file
const encryptedFile = await filesFromPaths([tempFilePath]);
const cid = await client.uploadFile(encryptedFile[0]);
console.log('cid', cid);
// clean up tmp file
fs.unlinkSync(tempFilePath);
}
async function getClient()
{
const client = await create();
const account = await client.login(process.env.LOGIN_ADDRESS);
const did = process.env.DID;
await account.provision(did);
await client.setCurrentSpace(did);
return client;
}
async function encryptFile() {
// Public keys
const publicKeysArmored = [
fs.readFileSync(process.env.PUBLIC_KEY1_PATH, 'utf8'),
fs.readFileSync(process.env.PUBLIC_KEY2_PATH, 'utf8'),
];
const publicKeys = await Promise.all(
publicKeysArmored.map((armoredKey) => openpgp.readKey({ armoredKey }))
);
// File data
const fileData = fs.readFileSync('sample_images/sample-image.png');
const fileMessage = await openpgp.createMessage({ binary: fileData });
const encrypted = await openpgp.encrypt({
message: fileMessage,
encryptionKeys: publicKeys,
format: "binary",
});
return encrypted;
}
await upload()
.catch(console.error)
.finally(() => process.exit())
動作デモ
上記コードを実際に動かしてみます。
まずは、ファイルを暗号化した上でWeb3.Storageにアップロードします。
❯❯❯ make encrypt-upload
node js/encrypt-upload.js
cid CID(bafkreiajim6gql4tziycfsn3m2c2rs4pkxifndj2re3jy5awaaxdmokkxi)
ファイルが暗号化されているため、そのままの状態ではブラウザで閲覧できない & ダウンロードしても開けないことを確認しましょう。
❯❯❯ make download URL=https://bafkreiajim6gql4tziycfsn3m2c2rs4pkxifndj2re3jy5awaaxdmokkxi.ipfs.w3s.link DOWNLOAD_FILE_NAME=encripted-sample-image.png
sh cmd/download.sh https://bafkreiajim6gql4tziycfsn3m2c2rs4pkxifndj2re3jy5awaaxdmokkxi.ipfs.w3s.link encripted-sample-image.png
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3242 100 3242 0 0 1582 0 0:00:02 0:00:02 --:--:-- 1586
次に、秘密鍵をimportしたPC上で、ファイルを復元してみます。
復元は、gpg --decrypt
コマンドを実施して行います。
❯❯❯ make decrypt ENCRIPTED_FILE_NAME=encripted-sample-image.png DECRYPTED_FILE_NAME=decripted-sample-image.png
sh cmd/decrypt.sh encripted-sample-image.png decripted-sample-image.png
gpg: ECDH鍵, ID 89E54450FC5F9CD4で暗号化されました
gpg: cv25519鍵, ID DAF2F738BAAD9868, 日付2023-12-16に暗号化されました
すると、暗号化されたファイルが復号され、PC上でアクセス可能になりました!
まとめ
Web3.Storageと暗号化技術を組み合わせることで、ファイルへのアクセスをコントロールし、安全に管理する方法を模索してみました。
ただし、より一般化するためには、gpg
コマンドを用いて復号する箇所などを抽象化し、画像アップロード・ダウンロード操作の過程でよりシームレスに実施できるようにする必要がありそうです。