原文:
zh.annas-archive.org/md5/71bd99f39f23fd60e3875318ad23711a
译者:飞龙 协议:CC BY-NC-SA 4.0
整个医疗行业都充斥着大量的纸质医疗记录,这导致了巨大的经济、时间和生命损失。电子医疗记录(EMRs)是纸质记录引起的许多问题的解决方案。有许多公司和研究人员正在利用区块链技术构建 EMR 数据管理和共享系统。我们将设计一种与互联网上的解决方案非常不同的解决方案,因为那些解决方案仅专注于匿名性、访问控制、安全性和隐私,而我们的解决方案还将通过实现跨应用程序通信来提供用户体验和大规模采用。在构建系统时,我们将学习如何使用 Proxy Re-Encryption(PRE)实现隐私。
在本章中,我们将学习以下主题:
电子医疗记录(EMRs)包含了医疗保健领域中的关键、高度敏感的私人信息,并且需要在同行之间频繁共享。EMR 数据管理和共享系统促进了不同参与者以安全可信的方式读取和写入 EMR 到系统中。这些系统应确保 EMR 数据的隐私、安全、可用性和细粒度的访问控制。EMRs 包括处方、实验室报告、账单以及在医院中找到的任何其他纸质记录。
一般来说,EMR 数据管理和共享系统允许医生开具数字处方,药店根据患者身份提取处方,实验室发出数字报告,患者查看所有记录并与他人共享等。
医疗记录需要在医疗过程中分发和共享给同行,如医疗服务提供者、保险公司、药店、研究人员和患者家属,这本身就是一个挑战。即使在共享后,这些记录在治疗过程中也需要不断更新。纸质记录也更容易丢失或放错位置。当有人患有严重的医疗状况,如癌症或艾滋病时,他们必须维护长时间的记录,因为这对治疗至关重要。使用纸质记录时,维护长时间的记录很麻烦。
此外,伪造的健康记录被提交给保险公司,导致保险公司巨大的财务损失。很多时候,医生和实验室也会在患者的同意下发布虚假处方和记录。例如,大学要求学生通过几项测试才能被录取,有时学生会试图获取假报告而不参加测试。
许多患者不购买自己的药物,也没有办法追踪患者是否服用了必要的药物。这导致患者生活质量受损,医疗系统成本增加。如果患者去不同的医生那里就诊,那么由于不同医生推荐不同类型的药物,患者出现有害副作用的可能性很大。如果一个人有多个医生为其治疗,那么这些医生就无法共同制定患者的药物管理计划,因此无法为所有相关方简化整个流程。由于患者未能提供他们的过去记录,当他们在不同的诊所就诊时,需要再次进行与特定化学品或物质相关的过敏测试,如果患者的医疗史得以保持,则无需进行此类测试。
处方中的潦草字迹也可能导致用药错误的风险。此外,由于远程通信时医生之间的口头交流,用药错误的可能性很大。此外,由于开具处方者对药物的期望剂量或多种药物之间的不良相互作用了解不足,也没有办法在纸质处方中实施警告和警报系统。
对于研究公司来说,收集和整理医疗记录用于研究目的是困难的。用药结束后无法续开处方,因此患者需要再次看医生,这是一个繁琐的过程。纸质处方无法实现在线购买药物,但数字化处方可以为在线药物配送打开大门,因为处方可以在线验证。
电子医疗记录(EMR)数据管理和共享系统应旨在解决之前的一些或所有问题。例如,在处方续开的情况下,第一步是数字化处方。然后,在患者的请求下,药房工作人员可以生成一个续开请求,该请求将被送达给开具处方的人员。开具处方的人员随后可以审查请求,并根据情况批准或拒绝请求。通过有限的资源利用和从开具处方者那里点击几下,他们就可以完成药物续开任务,同时增强持续的患者文档。
前述问题只是纸质记录带来的一些问题之一。但整个医疗保健行业充斥着大量由纸质记录引起的问题。解决方案应设计成能够解决这些问题,并且可以定期增强以解决额外的问题。
尽管电子病历数据管理和共享系统解决了很多问题,但它也有一些限制,影响了它的适应性和人们对其的信任。以下是一些限制:
无论 EMR 管理系统是集中式还是分散式,它都必须符合卫生当局的法律法规。由于这是敏感的公共数据,应该有一个监管机构来定义标准并规定数据的分享和存储规则。例如,1996 年的《健康保险移植和责任法案》(HIPAA)是美国的一项立法,为保障医疗信息的数据隐私和安全提供了规定。同样,不同国家有不同的立法。
要理解集中式 EMR 数据管理应用程序的问题,让我们以谷歌健康集中服务为例。谷歌健康服务允许用户手动添加他们的健康记录到应用中,或者通过登录到谷歌合作的健康服务提供者来添加。应用中存储的记录可能包括健康状况、药物、过敏以及实验室结果。谷歌健康利用这些信息为用户提供关于医疗状况、药物、状况和过敏之间可能的互动的信息。
2011 年,谷歌宣布将于 2012 年 1 月 1 日停止谷歌健康服务。数据可通过下载方式获取直至 2013 年 1 月 1 日。谷歌放弃该项目的原因是缺乏普遍的接受程度。
这表明我们很难相信集中式应用程序,因为它们可以随时终止服务。作为患者,这是一个大问题,因为突然间你不得不寻找其他选项来管理你的记录。即使是医院和其他卫生服务提供者也将不得不改变他们的系统。即使你想转换到另一个应用,迁移数据也不容易。许多谷歌健康(Google Health)的客户转向使用微软健康宝库(Microsoft HealthVault),这是一个竞争性的服务。微软发布了一个工具,让谷歌健康的客户将他们的个人健康信息转移到微软健康宝库账户。但是如果微软也停止他们的服务呢?因此,私人公司开发的集中式卫生应用程序是不值得信赖和采用的。
因此,许多政府都推出了他们自己的集中服务。爱沙尼亚的电子处方服务就是一个例子。政府服务是可以信赖的,采用并不是问题,因为政府可以强制卫生服务提供者使用该服务。这是他们强制的权威。但问题是一个应用无法解决所有问题,提供最佳功能集,拥有最佳用户体验等等。
政府和私人的集中式应用都存在问题,例如,在出现安全漏洞时,黑客可能危及集中服务中的所有公共记录。此外,有什么保证健康记录不会被修改,或者中央服务器不会删除某些记录呢?
前述问题意味着我们需要设计一个使用区块链的分散系统,其中区块链仅用于 EMR 访问控制和身份管理,而 EMR 位于中央化和分布式存储中。连接到该网络的所有应用程序都可以相互通信并共享数据。用户可以轻松切换应用程序,卫生管理机构将能够监管和监控网络。例如,两个不同的服务提供商可以构建具有不同功能和用户体验集合的不同应用程序,但是不同应用程序的用户可以读取/写入每个应用程序的 EMR。
卫生管理机构决定谁可以加入网络并提供医疗应用程序。为了加入网络,卫生管理机构可以设定一个预先检查的标准和措施清单,应用程序必须符合这些标准和措施才能加入网络。
在进一步进行并构建我们的分布式 EMR 数据管理和共享系统之前,让我们先了解一下 PRE 是什么。在我们的解决方案中,我们将使用 PRE 来确保安全和隐私。
PRE 是一组算法,允许您使用您的密钥对一些文本进行加密,然后更改密文,以便另一方可以解密,而不会透露您的密钥。要更改密文,您需要另一方的私钥或公钥,具体取决于您是使用交互式还是非交互式 PRE 算法。无论使用哪种算法,PRE 都涉及生成重新加密密钥,该密钥用于重新加密数据。重新加密密钥是基于所有者的私钥和接收者的私钥或公钥生成的,具体取决于算法类型。
在实践中,PRE 用于在第三方服务器上存储敏感数据,并允许您决定谁可以访问数据,而不会向第三方服务器透露实际数据。PRE 允许第三方(代理)更改已为一方加密的密文,以便另一方可以解密。
与其简单地与接收方共享您的私钥(不安全)或者为每个接收方分别加密整个消息n次,PRE 允许您仅加密数据一次,然后基于接收方的公钥委派对其进行访问。这消除了数据所有者必须在线的要求(数据可以存储在您不必管理的不同服务器上),并且还便于撤销访问权限(要阻止访问,您可以再次运行 PRE 来更改您的密钥,然后删除旧的密文)。
NuCypher PRE 当前支持的 PRE 算法是 BBS98。BBS98 基于椭圆曲线密码学。该库默认使用 secp256k1 曲线。请注意,以太坊帐户也使用相同的曲线(secp256k1),因此我们可以使用以太坊帐户密钥与 NuCypher。
目前,PRE 领域仍在积极研究和开发中。目前还没有多少用于 PRE 的库可用。你会发现基于 Java 或 Python 的交互式 PRE 库,但对于非交互式或基于对称密钥的,你找不到任何库。由于这个限制,我们将坚持使用微服务架构,并将所有代理重加密代码移至基于 Python 的微服务。
NuCypher 是一家构建去中心化的 PRE 服务产品的公司,称为 NuCypher 密钥管理服务(KMS)。NuCypher KMS 是一种去中心化的 KMS、加密和访问控制服务。它使得在公共网络中任意数量的参与者之间进行私密数据共享成为可能,使用 PRE 来委托解密权限,这是传统的对称或公钥加密方案无法实现的。原生代币用于激励网络参与者执行密钥管理和访问委托/撤销操作。
我们不会深入研究 NuCypher KMS,也不会在本书中使用它。相反,我们将探索如何使用 NuCypher 构建的 PRE 库。NuCypher 提供了 Python 和 Java 的 PRE 库,但我们只会学习如何使用 Python PRE 库。
NuCypher 不是 PRE 的唯一 Python 库。还有其他一些库可用。例如,ZeroDB 也提供了一个支持 AFGH 算法的 PRE 库,这是一个非交互式 PRE 算法。你可以在这里了解更多信息。
此库需要 python3
、libssl-dev
和 libgmp-dev
作为先决条件。要在 Ubuntu 上安装这些,请运行以下命令:
sudo apt-get install build-essential
sudo apt-get install python3
sudo apt-get install python3-dev libssl-dev libgmp-dev
在 macOS 上使用以下命令:
brew install python3
brew install gmp
现在让我们安装 PRE 库。要安装它,请运行以下命令:
git clone https://github.com/nucypher/nucypher-pre-python.git
cd nucypher-pre-python
pip3 install -e .
让我们看一个如何使用这个库的例子。该库仅支持交互式算法;它需要发送方了解接收方的私钥。
我们将创建一个示例 Python 脚本,其中 Alice 将加密一些文本,Bob 将与 Alice 分享他的私钥,Alice 将使用 Bob 的私钥创建一个派生密钥,然后 代理 将使用派生密钥进行再加密,最后 Bob 将使用他的私钥解密再加密的数据:
以下是这些交互的代码:
# Import bbs98 from NuCypher PRE
from npre import bbs98
# Initialize the re-encryption object
pre = bbs98.PRE()
# 'sk' means "secret key", and 'pk' means "public key"
# Alice's Private key
sk_a = pre.gen_priv(dtype=bytes)
# Alice's Public Key
pk_a = pre.priv2pub(sk_a)
# Bob's Private Key
sk_b = pre.gen_priv(dtype=bytes)
# Bob's Public Key
pk_b = pre.priv2pub(sk_b)
# Print Alice's Private Key as Hex String
print(sk_a.hex()[2:])
# Print Bob's Private Key as Hex String
print(sk_b.hex()[2:])
# Encrypt Message using Alice's Public Key
emsg = pre.encrypt(pk_a, "Hello World")
# Generate Re-Encrypt Key using Private key of sender and receiver
re_ab = pre.rekey(sk_a, sk_b)
# Re-Encrypt Message using Re-Encrypt key
emsg_b = pre.reencrypt(re_ab, emsg)
# Decrypt the message using Bob's Private Key
dmsg = pre.decrypt(sk_b, emsg_b)
# Print Decrypted Message
print(dmsg.decode("utf-8"))
上述代码不言自明。但上述场景中的问题是,为了让 Alice 将数据访问权限授予 Bob,Alice 需要知道 Bob 的私钥。这不是理想的情况,Bob 可能不愿意分享他的密钥。例如,如果 Bob 在使用相同的密钥进行区块链交易,那么他肯定不希望与 Alice 共享密钥。
幸运的是,有一个变通方法:这个技巧涉及到 Alice 生成一个新的密钥对,给予访问该密钥对的权限,然后用 Bob 的公钥加密该密钥对并分享。我们将在本章后面实际看到这一点。
让我们为使医疗应用程序能够共享数据而设计 DApp 的架构。基本上,使用不同医疗应用程序的用户可以相互分享 EMR。
该应用程序的生态系统将由医疗服务提供商(如医院、实验室和保险公司)、患者、应用程序提供商(将构建与此区块链网络集成的医疗应用程序的公司)以及网络管理机构或管理员(卫生部门和/或解决方案提供商)组成。
下图显示了高级别的架构:
这就是前述架构的工作原理:
仍然存在一个问题:在授予某人访问权限后,如何撤销访问权限?当然,您可以在区块链上撤销访问权限,但如果云服务器仍然向服务提供商提供您的新 EMR 访问权限怎么办?云服务器这样做的可能性很小,因为云服务器没有任何激励这样做。但患者可以通过更改其密钥并在其数据上运行 PRE 来避免这种情况。这将使您分享的所有重新加密密钥无效,因此以前授予访问权限的现有服务提供商无法读取新的 EMR。
让我们编写智能合约,负责注册患者和服务提供者的身份,并提供访问控制。
这是智能合约代码:
pragma solidity ⁰.4.22;
contract Health {
address owner;
struct ServiceProvider {
string publicKey;
}
struct Permission {
bool read;
bool write;
string reEncKey; //Re-Encrypt Key
}
struct Token {
int status;
bool read;
bool write;
string reEncKey; //Re-Encrypt Key
}
struct EMR {
string hash;
address issuer;
}
struct Patient {
string publicKey;
mapping (address => Permission) permissions;
mapping (bytes32 => Token) tokens;
bool closed;
EMR[] EMRs;
}
mapping (address => ServiceProvider) serviceProviders;
mapping (address => Patient) patients;
event tokenVerified (bytes32 hash, address patient, address
serviceProvider);
event reEncKeyAdded (address patient, address serviceProvider);
event patientAccountChanged(address oldAccountAddress, string
oldAccountPublicKey, address newAccountAddress, string
newAccountPublicKey, string reEncKey);
event emrAdded(address patient, address serviceProvider,
string emrHash);
constructor() {
owner = msg.sender;
}
//Utilities
function fromHexChar(uint c) public pure returns (uint) {
if (byte(c) >= byte('0') && byte(c) <= byte('9')) {
return c - uint(byte('0'));
}
if (byte(c) >= byte('a') && byte(c) <= byte('f')) {
return 10 + c - uint(byte('a'));
}
if (byte(c) >= byte('A') && byte(c) <= byte('F')) {
return 10 + c - uint(byte('A'));
}
}
function fromHex(string s) public pure returns (bytes) {
bytes memory ss = bytes(s);
require(ss.length%2 == 0); // length must be even
bytes memory r = new bytes(ss.length/2);
for (uint i=0; i<ss.length/2; ++i) {
r[i] = byte(fromHexChar(uint(ss[2*i])) * 16 +
fromHexChar(uint(ss[2*i+1])));
}
return r;
}
//Register Patient
function addPatient(string publicKey) returns (int reason) {
if(address(keccak256(fromHex(publicKey))) == msg.sender) {
patients[msg.sender].publicKey = publicKey;
}
}
//Register Service provider
function addServiceProvider(string publicKey) {
if(address(keccak256(fromHex(publicKey))) == msg.sender) {
serviceProviders[msg.sender].publicKey = publicKey;
}
}
//Patient:
//In QRCode include token string, address and private key
//Adds the hash of token and derivation key in Blockchain
function addToken(bytes32 hash, bool read, bool write, string reEncKey) {
if(patients[msg.sender].tokens[hash].status == 0 &&
patients[msg.sender].closed == false) {
patients[msg.sender].tokens[hash].status = 1;
patients[msg.sender].tokens[hash].read = read;
patients[msg.sender].tokens[hash].write = write;
patients[msg.sender].tokens[hash].reEncKey = reEncKey;
}
}
//Service Provider proves the token to get access
function requestAccess(string token, address patient) {
bytes32 hash = sha256(token);
if(patients[patient].tokens[hash].status == 1) {
patients[patient].tokens[hash].status = 2;
patients[patient].permissions[msg.sender].read =
patients[patient].tokens[hash].read;
patients[patient].permissions[msg.sender].write =
patients[patient].tokens[hash].write;
patients[patient].permissions[msg.sender].reEncKey =
patients[patient].tokens[hash].reEncKey;
tokenVerified(hash, patient, msg.sender);
}
}
//Add EMR
function addEMR(address patient, string hash) {
if(patients[patient].permissions[msg.sender].write == true) {
patients[patient].EMRs.push(EMR(hash, msg.sender));
emrAdded(patient, msg.sender, hash);
}
}
function getPatientPublicKey(address patient) returns
(string publicKey) {
return patients[patient].publicKey;
}
function isPatientProfileClosed(address patient) returns
(bool isClosed) {
return patients[patient].closed;
}
function getServiceProviderPublicKey(address serviceProvider)
returns (string publicKey) {
return serviceProviders[serviceProvider].publicKey;
}
//Revoke Access. Here you aren't changing the key.
function revokeServiceProviderAccess(address serviceProvider) {
patients[msg.sender].permissions[serviceProvider].read = false;
patients[msg.sender].permissions[serviceProvider].write =
false;
}
function getPermission(address patient, address serviceProvider)
returns(bool read, bool write, string reEncKey) {
return (patients[patient].permissions[serviceProvider].read,
patients[patient].permissions[serviceProvider].read,
patients[patient].permissions[serviceProvider].reEncKey);
}
function getToken(address patient, bytes32 hash) returns (int
status, bool read, bool write, string reEncKey) {
return (patients[patient].tokens[hash].status,
patients[patient].tokens[hash].read,
patients[patient].tokens[hash].write,
patients[patient].tokens[hash].reEncKey);
}
//Change your keys to revoke old account and move EMRs to new
// account.
function changePatientAccount(string reEncKey,
address newAddress, string newPublicKey) {
patients[msg.sender].closed = true;
if(address(keccak256(fromHex(newPublicKey))) == newAddress) {
patients[newAddress].publicKey = newPublicKey;
patientAccountChanged(msg.sender,
patients[msg.sender].publicKey, newAddress,
newPublicKey, reEncKey);
}
}
}
前述智能合约中的大部分代码都是不言自明的。在注册患者和服务提供者时,我们正在传递公钥,并验证公钥是否正确。address(keccak256(fromHex(publicKey))
短语计算publicKey
的address
,changePatientAccount
用于更改用户的帐户密钥,以防密钥被泄露。例如,如果您的应用提供商的服务器遭到黑客攻击并且您的私钥泄露了,应用提供商可以使用此功能来停用以前的帐户并为用户生成新帐户。云服务器将查找patientAccountChanged
事件,并对加密的 EMR 运行重新加密,以便您可以使用新密钥访问它们。然后它将删除旧的加密 EMR。用户还可以使用此功能向所有服务提供商撤销对 EMR 的访问权限。
现在让我们编写一些测试脚本来测试智能合约和数据和用户流。我们将编写 Python 脚本来加密数据、解密数据、生成重新加密密钥和重新加密数据。我们将使用 Node.js 调用 Python 脚本和智能合约函数。
创建一个名为test
的目录。在其中,创建一个名为encrypt.py
的文件,并将以下代码放入其中:
from npre import bbs98
pre = bbs98.PRE()
import base64
import sys
publicKey = base64.b64decode(sys.argv[1])
encrypted_message = pre.encrypt(publicKey, sys.argv[2])
print(base64.b64encode(encrypted_message))
此脚本接受两个参数,publicKey
和原始消息。 publicKey
作为base64
编码的公钥传递。此脚本将公钥转换为字节,以便npre
库可以利用它。最后,它加密消息并将其打印为base64
编码的密文。
创建另一个名为decrypt.py
的文件,并将以下代码放入其中:
from npre import bbs98
pre = bbs98.PRE()
import base64
import sys
privateKey = base64.b64decode(sys.argv[1])
encrypted_message = base64.b64decode(sys.argv[2])
decrypted_message = pre.decrypt(privateKey, encrypted_message)
print(decrypted_message)
这段代码负责解密。现在,创建另一个名为generate_reEncKey.py
的文件,并将以下代码放入其中:
from npre import bbs98
pre = bbs98.PRE()
import base64
import sys
base64_privateKeyA = base64.b64decode(sys.argv[1])
base64_privateKeyB = base64.b64decode(sys.argv[2])
re_ab = pre.rekey(base64_privateKeyA, base64_privateKeyB)
print(base64.b64encode(re_ab))
这段代码负责生成重新加密密钥。现在,创建另一个名为re_encrypt.py
的文件,并将以下代码放入其中:
from npre import bbs98
pre = bbs98.PRE()
import base64
import sys
reEncryptKey = base64.b64decode(sys.argv[1])
encrypted_message = base64.b64decode(sys.argv[2])
re_encrypted_message = pre.reencrypt(reEncryptKey, encrypted_message)
print(base64.b64encode(re_encrypted_message))
这段代码负责重新加密密文。现在创建一个 package.json
文件,用于保存我们 Node.js 应用程序的依赖项。将以下内容放入文件中,并运行 npm install
命令来安装模块:
{
"name": "health",
"private": true,
"dependencies": {
"eth-crypto": "¹.2.1",
"ethereumjs-tx": "~1.3.4",
"ethereumjs-util": "~5.2.0",
"ethereumjs-wallet": "~0.6.0",
"sha256": "~0.2.0",
"web3": "⁰.20.6",
"child_process": "~1.0.2"
}
}
现在,最后,创建一个名为 app.js
的文件,并在其中放置以下测试代码:
let Web3 = require("web3");
let ethereumjsWallet = require("ethereumjs-wallet")
let ethereumjsUtil = require("ethereumjs-util");
let ethereumjsTx = require("ethereumjs-tx");
let sha256 = require("sha256");
let EthCrypto = require('eth-crypto');
let exec = require("child_process").exec;
let web3 = new Web3(new
Web3.providers.HttpProvider("http://localhost:8545"));
let healthContract = web3.eth.contract([]);
let health = healthContract.new({
from: web3.eth.accounts[0],
data: '0x608060aa31862e....',
gas: '4700000'
}, function(e, contract) {
if (typeof contract.address !== 'undefined') {
let healthContractInstance = healthContract.at(contract.address);
//Generate Patient's Keys
let patient_wallet = ethereumjsWallet.generate();
//Register the Patient on blockchain.
let data = healthContractInstance.addPatient.getData
(patient_wallet.getPublicKey().toString('hex'));
let nonce = web3.eth.getTransactionCount
(patient_wallet.getAddressString())
let rawTx = {
gasPrice: web3.toHex(web3.eth.gasPrice),
gasLimit: web3.toHex(4700000),
from: patient_wallet.getAddressString(),
nonce: web3.toHex(nonce),
data: data,
to: contract.address
};
let privateKey = ethereumjsUtil.toBuffer("0x" +
patient_wallet.getPrivateKey().toString('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);
web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'),
function(error, result) {
if (error) {
console.log(error)
res.status(500).send({
error: "An error occured"
})
} else {
console.log("Patient Pub Key: " +
healthContractInstance.getPatientPublicKey.call
(patient_wallet.getAddressString()))
//Generate Service Provider's Keys
let hospital_wallet = ethereumjsWallet.generate();
//continue from here
}
})
}
})
编译智能合约,并将 ABI 和字节码分别填充到 healthContract
和 health
变量中。
这是前面代码的工作原理:
ethereumjs
库来创建离线账户,并使用这些账户进行交易签名。
child_process
来从 Node.js 执行 Python 脚本。尽管您可以使用 RESTful API 并采用微服务架构,但出于测试目的,这样做是可以的。
EthCrypto
来压缩和解压缩公钥。由 ethereumjs-wallet
生成的公钥是未压缩的,而由 npre
生成和使用的公钥是压缩的。私钥始终为 32 字节,公钥始终为 65 字节(或者压缩公钥为 33 字节)。公钥哈希始终为 20 字节。 npre
还在私钥的开头添加了 0x00
,在公钥的开头添加了 0x01
。
现在,在我们有一个续行注释的地方插入以下代码:
//Generate Service Provider's Keys
let hospital_wallet = ethereumjsWallet.generate();
//Register the Service Provider on blockchain
let data = healthContractInstance.addServiceProvider.getData
(hospital_wallet.getPublicKey().toString('hex'));
let nonce = web3.eth.getTransactionCount
(hospital_wallet.getAddressString())
let rawTx = {
gasPrice: web3.toHex(web3.eth.gasPrice),
gasLimit: web3.toHex(4700000),
from: hospital_wallet.getAddressString(),
nonce: web3.toHex(nonce),
data: data,
to: contract.address
};
let privateKey = ethereumjsUtil.toBuffer("0x" +
hospital_wallet.getPrivateKey().toString('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);
web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'),
function(error, result) {
if (error) {
console.log(error)
} else {
console.log("Hospital Pub Key: " +
healthContractInstance.getServiceProviderPublicKey.call
(hospital_wallet.getAddressString()))
let token = "yr238932";
let tokenHash = "0x" + sha256(token);
//Generate private key like npre. It has a extra character 0x00
//in beginning
let secKeyA = Buffer.concat([new Buffer([0x00]),
patient_wallet.getPrivateKey()]).toString('base64')
//Generate another private key to share with service provider
let temp_wallet = ethereumjsWallet.generate();
let secKeyB = Buffer.concat([new Buffer([0x00]),
temp_wallet.getPrivateKey()]).toString('base64')
exec('python3 ./generate_reEncKey.py ' + secKeyA + " " + secKeyB,
(error, stdout, stderr) => {
if (error !== null) {
console.log(error)
} else {
let reEncKey = stdout.substr(2).slice(0, -2)
console.log("Re-Encryption Key: " + reEncKey)
//Add token to blockchain
let data = healthContractInstance.addToken.getData
(tokenHash, true, true, reEncKey);
let nonce = web3.eth.getTransactionCount
(patient_wallet.getAddressString())
let rawTx = {
gasPrice: web3.toHex(web3.eth.gasPrice),
gasLimit: web3.toHex(4700000),
from: patient_wallet.getAddressString(),
nonce: web3.toHex(nonce),
data: data,
to: contract.address
};
let privateKey = ethereumjsUtil.toBuffer("0x" +
patient_wallet.getPrivateKey().toString('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);
web3.eth.sendRawTransaction("0x" +
tx.serialize().toString('hex'),
function(error, result) {
if (error) {
console.log(error)
} else {
console.log("Token Info: " +
healthContractInstance.getToken.call
(patient_wallet.getAddressString(), tokenHash, {
from: patient_wallet.getAddressString()
}))
//Get access to patient's data
let data =
healthContractInstance.requestAccess.getData
(token, patient_wallet.getAddressString());
let nonce = web3.eth.getTransactionCount
(hospital_wallet.getAddressString())
let rawTx = {
gasPrice: web3.toHex(web3.eth.gasPrice),
gasLimit: web3.toHex(4700000),
from: hospital_wallet.getAddressString(),
nonce: web3.toHex(nonce),
data: data,
to: contract.address
};
let privateKey = ethereumjsUtil.toBuffer("0x" +
hospital_wallet.getPrivateKey().toString('hex'),
'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);
web3.eth.sendRawTransaction("0x" +
tx.serialize().toString('hex'),
function(error, result) {
if (error) {
console.log(error)
} else {
console.log("Permission Info: " +
healthContractInstance.getPermission.call
(patient_wallet.getAddressString(),
hospital_wallet.getAddressString(), {
from: hospital_wallet.getAddressString()
}))
}
})
}
})
}
})
}
})
在这里,我们生成了一个临时密钥对,并假设它与服务提供商共享。然后,我们使用患者的私钥和临时私钥生成了一个重新加密密钥。然后,我们从患者的钱包执行了一个 addToken
交易和一个从服务提供商的钱包执行了一个 requestAccess
交易。这两个交易为服务提供商提供了访问患者 EMR 的权限。
现在,在我们有一个续行注释的地方插入以下代码:
let emr = JSON.stringify({
"Blood Group": "O+",
"type": "Blood Report"
});
let emrHash = sha256(emr);
let data = healthContractInstance.addEMR.getData
(patient_wallet.getAddressString(), emrHash);
let nonce = web3.eth.getTransactionCount
(hospital_wallet.getAddressString())
let rawTx = {
gasPrice: web3.toHex(web3.eth.gasPrice),
gasLimit: web3.toHex(4700000),
from: hospital_wallet.getAddressString(),
nonce: web3.toHex(nonce),
data: data,
to: contract.address
};
let privateKey = ethereumjsUtil.toBuffer("0x" + hospital_wallet.getPrivateKey().toString('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);
web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'),
function(error, result) {
if (error) {
console.log(error)
} else {
//Generate Public Key like npre. It's compressed and has a
//extra character 0x01 in beginning
let compressedPublicKey = Buffer.concat
([new Buffer([0x01]), Buffer.from(EthCrypto.publicKey.compress
(patient_wallet.getPublicKey().toString("hex")),
'hex')]).toString("base64")
exec('python3 ./encrypt.py ' + compressedPublicKey + " '" +
emr + "'", (error, stdout, stderr) => {
if (error !== null) {
console.log(error)
} else {
//Assume we are pushing encrypted data to proxy
//re-encryption server
let encryptedEMR = stdout.substr(2).slice(0, -2);
console.log("Encrypted Message: " + encryptedEMR)
//Assume that proxy re-encryption server re-encrypting
// data when requested by authorized service provider
exec('python3 ./re_encrypt.py ' + reEncKey + " " +
encryptedEMR, (error, stdout, stderr) => {
if (error !== null) {
console.log(error)
} else {
let reEncryptedEMR = stdout.substr(2).slice(0, -2)
console.log("Re-Encrypted Message: " + reEncryptedEMR)
//Assume service provider decrypting the re-encrypted
//data provided by the proxy re-encryption server
exec('python3 ./decrypt.py ' + secKeyB + " " +
reEncryptedEMR, (error, stdout, stderr) => {
if (error) {
console.log(error)
} else {
let decrypted_message = stdout.substr(2).slice(0, -2)
console.log("Decrypted Message: " + decrypted_message)
//Generate a new key for patient
let new_patient_wallet = ethereumjsWallet.generate();
let secKeyA = Buffer.concat([new Buffer([0x00]),
patient_wallet.getPrivateKey()]).toString('base64')
let secKeyB = Buffer.concat
([new Buffer([0x00]),
new_patient_wallet.getPrivateKey()]
).toString('base64')
exec('python3 ./generate_reEncKey.py ' + secKeyA + " "
+ secKeyB, (error, stdout, stderr) => {
if (error !== null) {
console.log(error)
} else {
let reEncKey = stdout.substr(2).slice(0, -2)
console.log("Re-encryption Key for Patient's new
Wallet: " + reEncKey)
//Change patient's key
let data = healthContractInstance.
changePatientAccount.getData
(reEncKey, new_patient_wallet.getAddressString(),
new_patient_wallet.getPublicKey().
toString('hex'));
let nonce = web3.eth.getTransactionCount
(patient_wallet.getAddressString())
let rawTx = {
gasPrice: web3.toHex(web3.eth.gasPrice),
gasLimit: web3.toHex(4700000),
from: patient_wallet.getAddressString(),
nonce: web3.toHex(nonce),
data: data,
to: contract.address
};
let privateKey = ethereumjsUtil.toBuffer("0x" +
patient_wallet.getPrivateKey().toString
('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);
web3.eth.sendRawTransaction("0x" +
tx.serialize().toString('hex'),
function(error, result) {
if (error) {
console.log(error)
} else {
let events = healthContractInstance.allEvents({
fromBlock: 0,
toBlock: 'latest'
});
events.get(function(error, logs) {
for (let count = 0; count < logs.length;
count++) {
console.log("Event Name: " +
logs[count].event + " and Args: " +
JSON.stringify(logs[count].args))
}
});
}
})
}
})
}
})
}
})
}
});
}
})
在这里,我们创建了一个表示血型的样本 EMR。然后我们将哈希放在区块链上,并假设将加密的 EMR 放在了云服务器上。然后,我们模拟了一个场景,云服务器重新加密了密文,服务提供商解密了密文。最后,我们生成了另一对密钥,并将患者的所有 EMR 移动到该账户,并关闭了旧账户。
所以,你看到了我们是如何模拟整个用户流程的,以及你如何使用 PRE 来确保安全性和隐私性。
在这一章中,我们学习了如何使用 PRE 在区块链中实现加密数据共享。在许多情况下,PRE 可以成为私有交易和 ZSL 的良好替代方案。我们所看到的架构可以应用于许多其他情况,其中敏感资产需要在对等方之间存储和共享。
除了 PRE 外,我们还了解了许多 JS 和 Python 库,比如etherumjs-wallet
,ethereumjs-tx
,ethereumjs-util
和npre
。我们还学习了如何发送原始交易,比如使用存储在 geth 节点外部的密钥签署交易的过程。在下一章中,我们将学习如何在 Quorum 中实现网络权限管理,以及如何使用手机号码构建转账解决方案。
如今,有许多由银行和其他金融科技公司开发的应用程序和服务,让我们可以发送和接受付款。但是我们还没有一个应用程序能够使发送和接收资金像发送和接收短信一样简单。虽然比特币和其他加密货币使得全球范围内的付款变得非常简单,但由于波动性和监管问题,它们目前无法成为主流。在本章中,我们将建立一个 P2P 支付系统,使发送和接收银行间支付变得非常容易,并且在银行之间的结算和清算几乎实时和简单。在构建解决方案的同时,我们还将学习各种银行和金融概念。
在本章中,我们将学习以下内容:
在本章中,我们将建立一个可集成在手机银行应用程序中的支付解决方案。这个解决方案将允许客户使用手机号码发送付款。只需使用手机号码就可以向世界上任何人发送付款将是发送付款的最友好方式。
我们的解决方案将使用数字化法定货币来进行银行间转账的结算和清算。为了理解为什么我们选择使用数字化法定货币作为结算媒介,让我们先了解一下银行间转账的结算和清算方式及其问题。
让我们首先了解国内银行间转账的工作原理。每个国家的中央银行都有一种或多种不同类型的集中式电子资金转移系统。例如,印度的即时付款服务(IMPS),美国的自动清算机构(ACH),加拿大的电子资金转账(EFT)。这些系统被各国银行用来向彼此发送消息,以促进向其客户的资金转移。只有消息被转移,而不是真正的资金。最终的结算通过结算账户进行。每家银行在中央银行都持有一个结算账户,当有转账消息时,资金要么在这些账户中存入,要么支出。为了更清楚地理解这一点,让我们看一个例子。
假设银行A在中央银行有一个结算账户,其中存入了50,000。同样,假设银行B在中央银行有一个结算账户,其中包含100,000。现在,假设X是银行A的客户,Y是银行B的客户。当X想向Y发送100 时,银行A通过资金转移系统向银行B发送一条消息,指示已从X的账户中扣除100,并将100 存入银行B的Y账户中。收到该消息后,银行B立即将Y的账户存入100。为了结算这笔款项,中央银行从银行A的结算账户中扣除100,因此新余额为49,900,并将100 存入银行B的结算账户,因此新余额为100,100。
中央银行通常在特定时间每天进行最终结算。消息传输几乎是实时的。只要涉及的两家银行都信任中央银行,这个过程就能正常运作。
让我们看看国际银行间转账是如何运作的。在这种情况下,涉及两个不同国家的两家银行,国际支付的方式与国内转账不同。在国际支付的情况下,银行使用 SWIFT 系统发送消息。SWIFT 是一种金融机构用于安全传输信息和指令的消息网络,通过标准化的代码系统进行传输。SWIFT 为每个金融组织分配一个唯一的代码,代码有 8 个或 11 个字符。该代码通常称为银行识别代码(BIC)、SWIFT 代码、SWIFT ID 或 ISO 9362 代码。
要了解更多关于 SWIFT 的信息,请访问www.investopedia.com/articles/personal-finance/050515/how-swift-system-works.asp
。
在这种情况下,两家银行不是与特定的中央银行拥有结算账户,而是彼此之间拥有结算账户。为了进一步理解这一点,让我们举个例子。假设银行A是一家美国银行,银行B是一家印度银行。X是银行A的客户,Y是银行B的客户。为了让X能向Y转账,反之亦然,银行A和银行B彼此之间持有结算账户。因此,银行A可能在银行B开设一个账户,其中存入了₹300,000,而银行B也会在银行A开设一个结算账户,其中存入了100,000。现在,当X向Y发送价值100 的付款时,银行B将从其管理的银行A的结算账户中扣除₹6909.50(本书编写时的汇率为 1 美元=69.10 印度卢比)。X的账户将被扣除
通常需要五到七天才能反映在Y的账户中。这是由于许多必要的流程、检查和问题,比如以下内容:
存放款项账户(Nostro)是银行A用来指代由银行B持有的我们的账户的术语。**您账户(Vostro)**是银行 B 使用的术语,其中存放着银行 A 的资金。
我们看到了银行间转账是如何运作的。对于国内转账,中央银行必须负责管理和更新结算账户,而对于国际转账,各银行必须努力更新结算账户。在国际转账的情况下,还存在其他问题,例如需要更多的对账工作,因为没有可信第三方,还有通过多个中介银行进行支付的路由。
区块链使银行能够通过提供数字化法定货币的能力直接将资金转移到世界上的任何其他银行;通过提供单一真相源,它减少了大量对账工作。
让我们来看看数字化法定货币在区块链上的过程和流程:
我们的支付应用将基于使用手机号码作为接收方的身份。让我们看看使用区块链将手机号码作为支付标识符的整个过程:
在我们继续编写智能合约之前,让我们为+1 ISD 代码的美元货币创建 Quorum 网络。我们将确保这些网络是经过许可的,并使用节点 ID 进行保护。
到目前为止,对于我们在本书中创建的所有网络,我们已经假设它们是使用白名单 IP 进行保护的。但是夸罗姆提供了一种方式来对节点 ID 进行白名单设置。你可以将相同的实践应用于本书中构建的其他网络。手机号码不应泄露到网络之外,因此重要性在于不惜一切代价保护网络。
网络许可是通过在节点启动期间将--permissioned
标志作为命令行参数添加到各个节点级别启用的。当添加该标志时,节点将在节点的数据目录文件夹中寻找名为permissioned-nodes.json
的文件。
permissioned-nodes.json
文件包含了该特定节点将接受来自和向外部连接的节点标识符(enode://nodeID@ip:port
)的列表。
如果设置了--permissioned
标志,但permissioned-nodes.json
文件为空或根本不存在于节点的数据目录文件夹中,则节点将启动,但它既不会连接到任何其他节点,也不会接受来自其他节点的任何传入连接请求。
例如,在我们的案例中,我们需要至少三个节点网络,即 A 银行,B 银行和中央银行。假设 A 银行的节点 ID 是480cd6ab5c7910af0e413e17135d494d9a6b74c9d67692b0611e4eefea1cd082adbdaa4c22467c583fb881e30fda415f0f84cfea7ddd7df45e1e7499ad3c680c
,B 银行的节点 ID 是60998b26d4a1ecbb29eff66c428c73f02e2b8a2936c4bbb46581ef59b2678b7023d300a31b899a7d82cae3cbb6f394de80d07820e0689b505c99920803d5029a
以及中央银行的节点 ID 是e03f30b25c1739d203dd85e2dcc0ad79d53fa776034074134ec2bf128e609a0521f35ed341edd12e43e436f08620ea68d39c05f63281772b4cce15b21d27941e
。
因此,Bank A 节点上的permissioned-nodes.json
文件将包含以下内容:
[
"enode://60998b26d4a1ecbb29eff66c428c73f02e2b8a2936c4bbb46581ef59b2678b7023d300a31b899a7d82cae3cbb6f394de80d07820e0689b505c99920803d5029a@[::]:23001?discport=0",
"enode://e03f30b25c1739d203dd85e2dcc0ad79d53fa776034074134ec2bf128e609a0521f35ed341edd12e43e436f08620ea68d39c05f63281772b4cce15b21d27941e@[::]:23002?discport=0"
]
类似地,银行B将白名单中的银行A和央行,而央行将白名单中的银行A和银行B。
permissioned-nodes.json
文件的任何添加都将在后续的传入/传出请求时动态地被服务器接收。节点不需要重新启动以使更改生效,但是从permissioned-nodes.json
文件中删除现有连接的节点不会立即断开这些现有连接的节点。但是,如果出于任何原因断开了连接,并且从已断开的节点 ID 发出了后续的连接请求,那么该请求将作为该新请求的一部分被拒绝。
让我们编写智能合约,将法定货币数字化并存储与银行账户关联的手机号。这是用于数字化法定货币的智能合约:
pragma solidity ⁰.4.18;
contract USD {
address centralBank;
mapping (address => uint256) balances;
uint256 totalDestroyed;
uint256 totalIssued;
event usdIssued(uint256 amount, address to);
event usdDestroyed(uint256 amount, address from);
event usdTransferred(uint256 amount, address from, address to,
string description);
function USD() {
centralBank = msg.sender;
}
function issueUSD(uint256 amount, address to) {
if(msg.sender == centralBank) {
balances[to] += amount;
totalIssued += amount;
usdIssued(amount, to);
}
}
function destroyUSD(uint256 amount) {
balances[msg.sender] -= amount;
totalDestroyed += amount;
usdDestroyed(amount, msg.sender);
}
function transferUSD(uint256 amount, address to, string
description) {
if(balances[msg.sender] >= amount) {
balances[msg.sender] -= amount;
balances[to] += amount;
usdTransferred(amount, msg.sender, to, description);
}
}
function getBalance(address account) returns (uint256 balance) {
return balances[account];
}
function getTotal() returns (uint256 totalDestroyed, uint256
totalIssued) {
return (totalDestroyed, totalIssued);
}
}
这是上述代码的工作原理:
USD
。这些方法都是不言自明的。转移方法还有一个描述,可以包含诸如交易目的或接收客户详细信息等信息。
USD
。
以下是保存手机号及其对应银行的智能合约:
pragma solidity ⁰.4.18;
contract MobileNumbers {
address centralBank;
struct BankDetails {
string name;
bool authorization;
}
mapping (address => BankDetails) banks;
mapping (uint256 => address[]) mobileNumbers;
event bankAdded(address bankAddress, string bankName);
event bankRemoved(address bankAddress);
event mobileNumberAdded(address bankAddress, uint256 mobileNumber);
function MobileNumbers() {
centralBank = msg.sender;
}
function addBank(address bank, string bankName) {
if(centralBank == msg.sender) {
banks[bank] = BankDetails(bankName, true);
bankAdded(bank, bankName);
}
}
function removeBank(address bank) {
if(centralBank == msg.sender) {
banks[bank].authorization = false;
bankRemoved(bank);
}
}
function getBankDetails(address bank) view returns (string
bankName, bool authorization) {
return (banks[bank].name, banks[bank].authorization);
}
function addMobileNumber(uint256 mobileNumber) {
if(banks[msg.sender].authorization == true) {
for(uint256 count = 0; count <
mobileNumbers[mobileNumber].length; count++) {
if(mobileNumbers[mobileNumber][count] == msg.sender) {
return;
}
}
mobileNumbers[mobileNumber].push(msg.sender);
mobileNumberAdded(msg.sender, mobileNumber);
}
}
function removeMobileNumber(uint256 mobileNumber) {
if(banks[msg.sender].authorization == true) {
for(uint256 count = 0; count <
mobileNumbers[mobileNumber].length; count++) {
if(mobileNumbers[mobileNumber][count] == msg.sender) {
delete mobileNumbers[mobileNumber][count];
//fill the gap caused by delete
for (uint i = count; i <
mobileNumbers[mobileNumber].length - 1; i++){
mobileNumbers[mobileNumber][i] =
mobileNumbers[mobileNumber][i+1];
}
mobileNumbers[mobileNumber].length--;
break;
}
}
}
}
function getMobileNumberBanks(uint256 mobileNumber) view returns
(address[] banks) {
return mobileNumbers[mobileNumber];
}
}
以下是上述代码的工作原理:
现在让我们看看用户支付和结算的整个流程:
USD
网络上有足够的USD
。
MobileNumbers
网络上注册其手机号。银行B将调用addMobileNumber
方法在网络上注册Y的银行账户。
getMobileNumberBanks
方法以获取 Y 拥有账户的银行列表。银行 B 必然会被列出,所以 X 可以选择它并点击“发送付款”按钮。
transferUSD
方法,并在说明中提供 Y 的手机号码,表示要将资金存入的银行账户。transferUSD
中的 to
地址将是由 getMobileNumberBanks
方法返回的地址。
在本章中,我们学习了一些银行业的基本概念,以及银行间转账是如何结算和清算的。我们还了解了 SWIFT 以及它的工作原理。然后我们深入了解了 Quorum 中的高级网络许可,并学习了 --permissioned
标志。
最后,我们建立了一种新型的资金转移系统,使用数字化的法定货币和客户识别的手机号进行支付结算。我们将整个解决方案构建在区块链上,这样可以最大程度地减少调节工作量,并解决了以前无法解决的许多问题。