#はじめに
Hyperledger Fabricのデータベースへ個人情報は保存すべきでない…周回遅れ過ぎる話題かもしれないが、啓蒙活動の意味で投稿する。
国内の個人情報保護法では、サービス利用者から個人情報の削除を申し立てられても、合意のもとで集められたものならば企業側は「削除義務」しか発生しない。しかしながら、全ての情報が合意のもとで集められたものでは無いかもしれないし、大きな企業ならばイメージもあるので削除できるに越したことはない。
欧州のGDPRはその合意もサービス利用者の都合で破棄できる(たぶん)から、個人情報をブロックチェーン上から抹消可能である必要がある。
#World Stateからは削除できる
DelStateメソッドを使えばWorld State(HLFで言うところのデータベース)からは削除することができる。しかし、取引履歴には残ってしまうため、データの完全削除は不可能である。以下で検証する。
#データの準備
検証のため戦国武将の個人情報(名前)と機密情報(石高)のデータを準備する。
参照先リンク
Key | 氏名 | 石高(万石) |
---|---|---|
0000000001 | 前田利長 | 120 |
0000000002 | 島津家久 | 73 |
0000000003 | 伊達政宗 | 62 |
0000000004 | 徳川義直 | 62 |
0000000005 | 徳川頼宣 | 56 |
0000000006 | 細川忠興 | 54 |
0000000007 | 黒田長政 | 47 |
0000000008 | 浅野幸長 | 43 |
0000000009 | 毛利輝元 | 36 |
0000000010 | 鍋島勝茂 | 36 |
当時の武将にとっては、自藩の石高は機密情報だったのではないだろうか。
隣の藩が5万石と思って攻め込んだら実は10万石のリソースがあって、倍の兵力で返り討ちにあったりすると大変である。今も昔も情報は大切。
#その石高を削ぐために参勤交代をしていたような
#データ定義
チェインコードはGolangで実装する。データ定義は次の通り。
type Asset struct {
Name string `json:"name"` // 氏名
Kokudaka int `json:"kokudaka"` // 石高(万石)
}
何のひねりもない。石高はResource
にしようかと考えたが、「万石」を単位にしたかったのでKokudaka
にしてしまった。「加賀百万石」みたいな響きがカコイイ。
ちなみに、データベースはCouchDBを使用する。
#データの登録、参照、更新、削除
func (s *SmartContract) createAsset(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
fmt.Println("createAsset:START")
if len(args) != 3 {
return shim.Error("Incorrect number of arguments. Expecting 3")
}
koku, _ := strconv.Atoi(args[2])
asset := Asset{Name: args[1], Kokudaka: koku}
fmt.Println("asset", asset)
assetAsBytes, _ := json.Marshal(asset)
APIstub.PutState(args[0], assetAsBytes)
fmt.Println("createAsset:END")
return shim.Success(nil)
}
func (s *SmartContract) queryAsset(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
fmt.Println("queryAsset:START")
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
fmt.Println("- queryAsset:key =", args[0])
assetAsBytes, _ := APIstub.GetState(args[0])
var buffer bytes.Buffer
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(args[0])
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
// Record is a JSON object, so we write as-is
buffer.WriteString(string(assetAsBytes))
buffer.WriteString("}")
fmt.Println("queryAsset:END")
return shim.Success(buffer.Bytes())
}
func (s *SmartContract) queryAllAsset(APIstub shim.ChaincodeStubInterface) sc.Response {
fmt.Println("queryAllAsset:START")
startKey := ""
endKey := ""
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()
// buffer is a JSON array containing QueryResults
var buffer bytes.Buffer
buffer.WriteString("[")
bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
// Add a comma before array members, suppress it for the first array member
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
// Record is a JSON object, so we write as-is
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("- queryAllAsset:\n%s\n", buffer.String())
fmt.Println("queryAllAsset:END")
return shim.Success(buffer.Bytes())
}
func (s *SmartContract) updateAsset(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
fmt.Println("updateAsset:START")
if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}
assetAsBytes, _ := APIstub.GetState(args[0])
asset := new(Asset)
json.Unmarshal(assetAsBytes, &asset)
koku, _ := strconv.Atoi(args[1])
asset.Kokudaka = koku
fmt.Println("asset", asset)
assetAsBytes, _ = json.Marshal(asset)
APIstub.PutState(args[0], assetAsBytes)
fmt.Println("updateAsset:END")
return shim.Success(nil)
}
#検証-1 データ登録、参照、更新
登録したデータを参照。
root@liva-z:tst# ./qallAsset.sh
[{"Key":"0000000001", "Record":{"kokudaka":120,"name":"前田利長"}},{"Key":"0000000002", "Record":{"kokudaka":73,"name":"島津家久"}},{"Key":"0000000003", "Record":{"kokudaka":62,"name":"伊達政宗"}},{"Key":"0000000004", "Record":{"kokudaka":62,"name":"徳川義直"}},{"Key":"0000000005", "Record":{"kokudaka":56,"name":"徳川頼宣"}},{"Key":"0000000006", "Record":{"kokudaka":54,"name":"細川忠興"}},{"Key":"0000000007", "Record":{"kokudaka":47,"name":"黒田長政"}},{"Key":"0000000008", "Record":{"kokudaka":43,"name":"浅野幸長"}},{"Key":"0000000009", "Record":{"kokudaka":36,"name":"毛利輝元"}},{"Key":"0000000010", "Record":{"kokudaka":36,"name":"鍋島勝茂"}}]
伊達政宗の石高を62から70へ変更して参照する。
root@liva-z:tst# ./updateAsset.sh 0000000003 70
2020-03-11 21:00:23.019 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
更新できたか確認。
root@liva-z:tst# ./queryAsset.sh 0000000003
{"Key":"0000000003", "Record":{"kokudaka":70,"name":"伊達政宗"}}
#検証-2 データ削除
伊達政宗のレコードを削除する。
root@liva-z:tst# ./deleteAsset.sh 0000000003
2020-03-11 21:06:06.284 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
削除できたか確認。
root@liva-z:tst# ./qallAsset.sh
[{"Key":"0000000001", "Record":{"kokudaka":120,"name":"前田利長"}},{"Key":"0000000002", "Record":{"kokudaka":73,"name":"島津家久"}},{"Key":"0000000004", "Record":{"kokudaka":62,"name":"徳川義直"}},{"Key":"0000000005", "Record":{"kokudaka":56,"name":"徳川頼宣"}},{"Key":"0000000006", "Record":{"kokudaka":54,"name":"細川忠興"}},{"Key":"0000000007", "Record":{"kokudaka":47,"name":"黒田長政"}},{"Key":"0000000008", "Record":{"kokudaka":43,"name":"浅野幸長"}},{"Key":"0000000009", "Record":{"kokudaka":36,"name":"毛利輝元"}},{"Key":"0000000010", "Record":{"kokudaka":36,"name":"鍋島勝茂"}}]
root@liva-z:tst# ./queryAsset.sh 0000000003
{"Key":"0000000003", "Record":}
Key=0000000003の伊達政宗レコードが無くなっている。
Futonでも確認。
やっぱり消えている。
データベースのレコードは削除されているのが確認できた。
#取引履歴を参照する
ブロックチェーンは、取引履歴が1トランザクション/1ブロックの単位でチェーン状に繋がっている(と理解している)。
この取引履歴を参照してみよう。
func (s *SmartContract) getHistoryForKey(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
fmt.Println("getHistoryForKey:START")
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
resultsIterator, err := APIstub.GetHistoryForKey(args[0])
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()
var buffer bytes.Buffer
buffer.WriteString("[")
bFlg := false
for resultsIterator.HasNext() {
response, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
if bFlg == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"TxId\":")
buffer.WriteString("\"")
buffer.WriteString(response.TxId)
buffer.WriteString("\"")
buffer.WriteString(", \"Value\":")
if response.IsDelete {
buffer.WriteString("null")
} else {
buffer.WriteString(string(response.Value))
}
buffer.WriteString("}")
bFlg = true
}
buffer.WriteString("]")
fmt.Println("getHistoryForKey:END")
return shim.Success(buffer.Bytes())
}
取引履歴を参照する。
root@liva-z:tst# ./getHistoryForKey.sh 0000000003
[{"TxId":"5c3841cae7c5845df9343e3a36665ed08eeae79dcd3d983834da1551970520f1", "Value":{"name":"伊達政宗","kokudaka":62}},{"TxId":"7ad3e93c91d2ff7f01eccb5b534507ef051e29c47ccf836083712aae8c6d6cf5", "Value":{"name":"伊達政宗","kokudaka":70}},{"TxId":"d229e2217dc00bc201557051a7fe300d848e1aab446287a7d6e13181dfbb470b", "Value":null}]
データの登録から更新、削除までの履歴が表示された。
この情報を削除することは、取引履歴を改ざんすることになるから不可能である。
これではシステム上から個人情報や機密情報を完全に削除したとは言えないだろう。
#おわりに
ブロックチェーンで機微な情報を記録する場合は、Private Dataへ記録するか外部RBDとKeyで紐付けて記録すべし。