跳转至

构建数字化病历的 DApp

整个医疗行业充斥着大量的纸质病历,这在金钱、时间和生命方面造成了巨大的损失。电子病历 ( EMRs )是纸质病历带来的诸多问题的解决方案。有许多公司和研究人员致力于使用区块链技术建立电子病历数据管理和共享系统。我们将设计一个与您在互联网上找到的解决方案非常不同的解决方案,在某种意义上,这些解决方案只关注匿名性、访问控制、安全性和隐私,而我们的解决方案还将通过支持跨应用程序通信来提供用户体验和大规模采用。在构建系统时,我们将学习如何使用代理重新加密 ( PRE )来实现隐私。

在本章中,我们将了解以下主题:

  • 什么是医疗保健数据管理系统,其功能是什么?
  • 纸质病历带来的问题和数字化病历的好处
  • 医疗数据管理系统的局限性?
  • 构建集中式医疗保健数据管理应用程序的问题
  • PRE 是什么以及它如何帮助在区块链中实现数据隐私
  • 如何构建一个支持跨应用程序数据共享的分散式医疗数据管理系统
  • 如何使用 Python 和 JavaScript 为 DApp 医疗保健数据管理构建智能合同和测试

电子病历数据管理和共享系统介绍

电子病历包含医疗保健领域的关键、高度敏感的私人信息,需要在同行之间频繁共享。EMR 数据管理和共享系统为不同的参与者向系统读写 EMR 提供了安全可靠的方法。这些系统应该确保 EMR 数据的隐私性、安全性、可用性和细粒度的访问控制。电子病历包括处方、化验报告、账单和任何其他你能在医院找到的纸质记录。

一般来说,EMR 数据管理和共享系统允许医生开具数字处方,允许药房根据患者的身份开具处方,允许实验室出具数字报告,允许患者查看所有记录并与其他人共享,等等。

纸质病历的问题

医疗记录需要在同行之间分发和共享,如医疗保健提供商、保险公司、药店、研究人员和患者家属,这本身就是一个挑战。即使共享后,这些记录也需要在治疗过程中不断更新。纸质记录也更容易丢失或放错位置。当有人患有严重的疾病,如癌症或艾滋病毒时,他们必须保留长期的历史记录,因为这对治疗至关重要。对于纸质记录,维护长历史记录很麻烦。

此外,伪造的健康记录被提交给保险公司,导致保险公司的巨大经济损失。很多时候,医生和实验室也会在患者同意的情况下,给患者开具假的处方和记录。例如,大学要求学生在被录取前通过几项测试,有时学生试图在不通过测试的情况下获得假报告。

许多患者不购买他们的药物,也没有办法跟踪患者是否已经消耗了必要的药物。这会损害患者的生活质量,并增加医疗保健系统的成本。如果一个病人去看不同的医生,那么由于不同的医生推荐不同类型的药物,就很有可能产生有害的副作用。如果一个人有多名医生为其治疗,那么这些医生就没有办法一起制定患者的药物管理计划,因此不可能为所有相关方简化整个流程。由于患者无法提供其过去的记录,因此在不同的诊所就诊时,需要反复进行与特定化学物质或物质引起的过敏相关的测试,如果保留患者的病史,则不需要这样做。

处方中糟糕的书写也会造成用药错误的风险。还有,由于医生之间存在远程沟通时的口头沟通,存在巨大的用药错误几率。此外,由于开处方者对药物的期望剂量的知识不足,或者多种药物之间的不期望的相互作用。纸质处方也没有办法实现警告和提示系统。

研究公司很难出于研究目的收集和组织医疗记录。吃完药就没办法续开处方了,所以患者会再次需要医生,这是一个繁琐的过程。纸质订阅未能实现在线购药,但数字订阅可以打开在线药物交付的大门,因为处方可以在线验证。

一个电子病历数据管理和共享系统应该致力于解决一些或所有以前的问题。例如,在处方更新的情况下,第一步是将处方数字化。然后,在患者的请求下,药房工作人员可以生成一个更新请求,并将其发送给开处方者。然后,开处方者可以审核请求,并相应地批准或拒绝请求。利用有限的资源,只需处方医生点击几下鼠标,他们就可以完成药物更新任务,同时增强连续的患者文档。

之前的问题只是基于纸张的记录所引起的问题中的几个。但是,由于纸质记录,整个医疗保健行业充满了大量的问题。解决方案的设计应该能够解决这些问题,并且可以定期增强以解决其他问题。

电子病历数据管理和共享系统的局限性

虽然电子病历数据管理和共享系统解决了许多问题,但它也有一些限制,影响了它的适应性和人们对它的信任。以下是一些限制:

  • 财务成本和投资回报:购买、实施、支持和维护这样一个系统的成本是无法承受的,尤其是对于小型医院和诊所。即使他们免费获得该系统,还有其他与界面管理、灵活性定制、培训、维护和升级相关的财务成本。

  • 提升劳动力:目前,劳动力沉迷于纸质记录。培训患者、医生、药房、医院等采用该解决方案是一项困难且耗时的任务。有时可能需要改变劳动力。例如,当银行开始实施计算机,从记录账簿过渡到数字记录时,许多人无法理解和采用它,因此失去了工作。

  • 数据输入的完整性:可能会出现意外的数据输入错误,例如选择了错误的患者或在剂量菜单中点击了错误的选项。
  • 安全和隐私:这是最重要的问题之一。健康记录需要安全存储,因为电子健康数据库总是黑客攻击的目标。健康记录包含非常敏感的信息,泄露可能会导致灾难。应实施严格的访问控制,并定期进行反馈。未经患者同意,他们的记录不应与任何人共享。
  • 系统停机:由于网络或硬件相关问题,系统可能会定期停机。无法使用该系统是一个很大的问题。
  • 患者无法访问:如果出现患者无法控制的情况,例如医疗服务提供者办公室的软件出现故障,患者将无法再向医疗服务提供者索要纸质处方,也无法前往药房获取所需药物。这使得病人处于技术人员或其他未被发现的工作人员的控制之下。

集中式与分散式电子病历管理系统

不管电子病历管理系统是集中的还是分散的,它必须符合卫生当局的法律。由于这是敏感的公共数据,应该有一个监管机构来定义标准,并规定如何共享和存储数据的规则。例如,1996 年的健康保险便携性和责任法案 ( HIPAA )是一项美国立法,它提供了保护医疗信息的数据隐私和安全条款。同样,不同的国家有不同的立法。

为了理解集中式 EMR 数据管理应用的问题,让我们以 Google Health centralized service 为例。谷歌健康服务允许用户手动或通过登录谷歌的合作健康服务提供商将他们的健康记录添加到应用程序中。存储在应用程序中的记录可能包括健康状况、药物、过敏和实验室结果。Google Health 利用这些信息向用户提供医疗条件以及药物、条件和过敏之间可能的相互作用的信息。

2011 年,谷歌宣布将于 2012 年 1 月 1 日停用谷歌健康。数据可下载至 2013 年 1 月 1 日。谷歌给出的放弃该项目的原因是缺乏广泛的采用。

这表明我们很难信任集中式应用程序,因为它们可以随时停止服务。作为一名患者,这是一个很大的问题,因为你突然不得不寻找一个不同的选项来管理你的记录。甚至医院和其他医疗服务提供者也将不得不改变他们的系统。即使你希望切换到不同的应用程序,迁移数据也不容易。许多谷歌健康客户转向使用微软 HealthVault,这是一项竞争服务。微软发布了一个工具,让谷歌健康客户将其个人健康信息转移到微软 HealthVault 帐户。但是如果微软也停止他们的服务呢?因此,由私人公司构建的集中式健康应用程序不能被信任和采用。

正因为如此,许多政府都提出了自己的集中式服务。这方面的一个例子是爱沙尼亚的电子处方服务。政府服务是可以信任的,采用也不是问题,因为政府可以强制医疗服务提供者使用这项服务。他们是授权这样做的权威。但问题是一个 app 不可能解决所有问题,提供最好的一组功能,拥有最好的用户体验等等。

政府和私人集中式应用程序都存在问题,例如,在出现安全漏洞的情况下,黑客可能会破坏集中式服务中的所有公共记录。此外,如何保证健康记录不会被修改,或者某些记录不会被中央服务器删除?

上述问题意味着我们需要设计一个使用区块链的分散式系统,其中区块链仅用于 EMR 访问控制和身份管理,并且 EMR 驻留在集中式和分布式存储中。连接到此网络的所有应用程序都可以相互通话并共享数据。用户可以轻松地在应用程序之间切换,卫生当局将能够监管和监控网络。例如,两个不同的服务提供商可以构建具有不同功能集和用户体验的不同应用,但是不同应用的用户可以读取/写入每个应用的 EMR。

卫生当局决定谁加入网络,可以提供医疗保健应用程序。为了加入网络,卫生当局可以设置应用程序为了能够加入网络而应该满足的标准和措施的预检列表。

使用 PRE 确保区块链中的数据隐私

在进一步构建我们的分散式 EMR 数据管理和共享系统之前,让我们先了解一下 PRE 是什么。在我们的解决方案中,我们将使用 PRE 来确保安全性和隐私性。

PRE 是一组算法,允许您用您的密钥加密一些文本,然后更改密文,以便它可以被另一方解密,而不会泄露您的密钥。要更改密文,您需要另一方的私钥或公钥,具体取决于您使用的是交互式还是非交互式 PRE 算法。不管是哪种算法,PRE 都会生成一个重新加密密钥,用于重新加密数据。基于算法的类型,基于所有者的私钥和接收者的私钥或公钥来生成重新加密密钥。

实际上,PRE 用于将敏感数据存储在第三方服务器上,并允许您决定谁可以访问数据,而无需将实际数据透露给第三方服务器。PRE 允许第三方(代理)更改为一方加密的密文,以便另一方可以解密。

PRE 允许你加密数据一次,然后根据接收者的公钥授权访问,而不是天真地与接收者共享你的私钥(不安全)或为每个接收者加密整个消息 n 次。这消除了对数据所有者在线的要求(数据可以存储在不同的服务器上,而您不必管理它),并且也方便了访问的撤销(要阻止访问,您可以再次运行 PRE 来更改您的密钥,然后删除旧的密文)。

NuCypher PRE 目前支持的 PRE 算法是 BBS98。BBS98 基于椭圆曲线加密技术。默认情况下,该库使用 secp256k1 曲线。请注意,以太坊帐户也使用相同的曲线(secp256k1),因此我们可以将以太坊帐户密钥与 NuCypher 一起使用。

目前,PRE 领域仍处于大量研究和开发中。PRE 可用的库不多。对于交互式 PRE,您会发现基于 Java 或 Python 的库,但是对于非交互式——或基于对称密钥的库,您不会发现任何库。由于这种限制,我们将坚持微服务架构,并将所有代理重新加密的代码移动到基于 Python 的微服务。

NuCypher PRE 图书馆

NuCypher 是一家建立了分散化 PRE 即服务产品的公司,名为 NuCypher 密钥管理服务 ( KMS )。KMS 是一个分散的 KMS、加密和访问控制服务。它允许在公共网络中任意数量的参与者之间共享私有数据,使用 PRE 以传统对称或公钥加密方案无法实现的方式委托解密权限。本地令牌用于激励网络参与者执行密钥管理和访问授权/撤销操作。

我们不会深入研究努西佛 this,也不会在本书中用到它。相反,我们将探索如何使用 NuCypher 构建的 PRE 库。NuCypher 为 Python 和 Java 提供了 PRE 库,但我们将只学习如何使用 Python PRE 库。

NuCypher 并不是 PRE 唯一可用的 Python 库。外面还有一些人。例如,ZeroDB 还提供了一个支持 AFGH 算法的预库,这是一个非交互式的预算法。您可以在 https://github.com/zerodb/zerodb-afgh-pre.了解更多信息

安装库

这个库需要python3libssl-devlibgmp-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

现在让我们安装预库。要安装它,请运行以下命令:

git clone https://github.com/nucypher/nucypher-pre-python.git
cd nucypher-pre-python
pip3 install -e .

使用图书馆

让我们看一个如何使用这个库的例子。该库仅支持交互式算法;它要求发送者知道接收者的私钥。

我们将创建一个示例 Python 脚本,其中 Alice 将加密一些文本, Bob 将与 AliceAlice 将使用 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 可能不愿意共享他的密钥。例如,如果鲍勃使用同一个密钥进行区块链交易,那么他肯定不想与爱丽丝共享该密钥。

幸运的是,有一个变通办法:这个技巧涉及到 Alice 生成一个新的密钥对,授予对该密钥对的访问权,然后通过用 Bob 的公钥加密来共享该密钥对。我们将在本章后面的实践中看到这一点。

为电子病历构建 DApp

让我们设计 DApp 的架构,使医疗保健应用程序能够相互共享数据。基本上,拥有不同医疗保健应用程序的用户可以相互共享电子病历。

该应用的生态系统将由医疗保健服务提供商(如医院、实验室和保险公司)、患者、应用提供商(将构建与区块链网络集成的医疗保健应用的公司)和网络机构或管理员(卫生机构和/或解决方案提供商)组成。

下图显示了高级架构:

前面的架构是这样工作的:

  • 网络管理员决定谁可以加入网络并连接到云服务器。

  • 区块链将保存服务提供商和患者的身份和许可,而集中式和分布式服务器将存储加密的电子病历。每个用户和服务提供商都将拥有自己的以太坊密钥来标识自己。我们不将加密的 EMR 存储在区块链中有两个原因:

    • 每一份数据都将被复制到区块链中的每个节点。这将损害可伸缩性,因为节点大小将急剧增加。
    • 根据 compliance 的说法,你甚至不应该共享加密的数据,因为加密算法可能在未来被破解,所有的数据都将被泄露。因此,如果它保存在一个中心位置,那么服务器可以立即被拔掉。
  • 每当有人从存储器中请求数据时,服务器将检查区块链,以查看患者是否已被授予访问权限,如果是,那么它将使用重新加密密钥重新加密数据,并将重新加密的数据交给接收者。
  • 当请求向云服务器读取或写入数据时,客户端应该对服务器提供的令牌进行签名,以证明其身份。基于此,云服务器将在区块链中寻找权限,并决定是否重新加密。客户端将使用他们的私有以太坊密钥进行签名。这是对云服务器的认证过程。
  • 用户将在区块链上注册他们的身份。如果患者想要访问他们的数据,那么他们将生成一个新的私钥和一个随机令牌。使用这个私钥,患者将生成一个重新加密密钥,并将其放在区块链上,供云服务器在重新加密数据时参考。用户不能使用服务提供商的密钥直接生成重新加密密钥,因为在这种情况下,服务提供商必须公开他们的私钥,因为我们使用的是交互式预算法。患者将随机令牌的散列值添加到区块链中,服务提供商必须发送一个事务,证明他们知道随机令牌充当用户共享访问的证据。这是一种授权数据访问的方法。例如,如果用户希望在手机上访问他们的电子病历,那么用户将生成一个 QR 码,其中有一个私钥和令牌。然后,他们会将交易发送到区块链,声明谁证明他们有令牌,谁就可以访问他们的数据。在医院,接待员可以简单地扫描二维码,并推动交易证明,以获取随机字符串,从而获得用户的数据。二维码也将持有私钥。每当患者想要给某人访问权限时,他们必须生成新的密钥对和令牌。

  • 一旦获得写访问权限,服务提供商就可以创建定义格式的 EMR,将它们的哈希放在区块链上,然后将使用患者公钥加密的 EMR 发送到云服务器进行存储。

  • 云服务器由卫生机构或解决方案提供商控制。它应符合卫生当局的要求,并应遵守标准。即使云服务器遭到黑客攻击,黑客也无法读取任何内容,因为存储在其中的所有内容都是加密的,密钥分布在各个应用提供商之间。
  • 该解决方案可以以这样一种方式进行扩展,即最终用户控制他们的私钥,这样他们就不必信任应用程序提供商。但这会损害用户体验,因为用户不习惯存储私钥。如果他们丢失了密钥,那么就永远无法访问他们的电子病历。
  • 尽管云服务器为访问 EMR 创建了集中化,但是您仍然可以信任它。密钥并不存储在云服务器中,它只是作为存储。即使云服务器在未经你允许的情况下将你的数据的访问权交给某人,接收方也无法读取数据,所以云服务器是可以信任的。该服务器可以是分布式的,以实现高可用性。
  • 该解决方案允许您使用来自不同应用程序提供商的不同医疗保健应用程序,并且仍然可以在每个应用程序上查看所有电子病历。要在新应用程序上导入所有 EMR,用户必须从以前的应用程序中导出密钥,并将其导入新应用程序。

仍然存在的一个问题是:在授予某人访问权限后,如何撤销访问权限?当然,您可以取消对区块链的访问,但是如果云服务器仍然允许服务提供商访问您的新电子病历呢?这种情况发生的可能性很小,因为云服务器不太可能有这样做的动机。但是,如果患者更改他们的密钥并对他们的数据运行 PRE,这是可以避免的。这将使您到目前为止共享的所有重新加密密钥失效,因此您过去允许访问的现有服务提供商无法读取新的 EMR。

身份和访问控制智能合约

让我们编写智能合同,负责注册患者和服务提供者的身份,并提供访问控制。

智能合约代码如下:

pragma solidity ^0.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.changePatientAccount计算出address用于更改用户的账户密钥,以防密钥泄露。例如,如果你的应用程序提供商的服务器遭到黑客攻击,你的私钥被泄露,这就可以使用;应用程序提供商可以使用此功能来停用以前的帐户,并为用户生成新帐户。云服务器将寻找patientAccountChanged事件,并对加密的电子病历进行重新加密,这样您就可以使用新的密钥访问它们。然后,它将删除旧的加密电子病历。这也可以被用户用来从所有服务提供商撤销对 EMR 的访问。

编写用于测试的 Python 和 JS 脚本

现在让我们编写一些测试脚本来测试智能合约以及数据和用户流。我们将编写 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": "^1.2.1",
        "ethereumjs-tx": "~1.3.4",
        "ethereumjs-util": "~5.2.0",
        "ethereumjs-wallet": "~0.6.0",
        "sha256": "~0.2.0",
        "web3": "^0.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 和字节码分别填充healthContracthealth变量。

以下是上述代码的工作原理:

  • 我们使用ethereumjs库创建离线账户,并使用这些账户签署交易。
  • 我们使用child_process从 Node.js 执行 Python 脚本。虽然您可以使用 RESTful APIs 并采用微服务架构,但出于测试目的,这很好。
  • 我们使用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交易。这两个交易为服务提供商提供了对患者电子病历的访问。

现在,在我们有延续注释的地方插入以下代码:

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 放在云服务器上。然后我们模拟一个场景,云服务器重新加密密文,服务提供商解密密文。最后,我们生成了另一个密钥对,并将患者的所有电子病历转移到该帐户,并关闭了旧帐户。

因此,您看到了我们如何模拟整个用户流,以及如何使用 PRE 来保护安全性和隐私。

摘要

在本章中,我们学习了如何使用 PRE 在区块链中启用加密数据共享。在许多情况下,PRE 可以是私人交易和 ZSL 的良好替代方案。我们看到的架构可以应用于许多其他用例,其中敏感资产需要在对等体之间存储和共享。

除了 PRE,我们还学习了很多 JS 和 Python 库,比如etherumjs-walletethereumjs-txethereumjs-utilnpre。我们还学习了如何发送原始事务,比如使用存储在 geth 节点之外的密钥对事务进行签名的过程。在下一章中,我们将介绍如何在 Quorum 中实现网络许可,以及如何构建一个使用手机号码转账的解决方案。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组