React Native

cryptoUtil

In the following sections, we will use different cryptographic primitives to implement parts of the PAD protocol. Here we provide a cryptoUtil.js script with these cryptographic primitives.
The secret sharing library is forked from secrets.js-grempe, with slight modifications for react native. The source code is provided in the bottom of this page.
1
import { RSA, KeyPair } from 'react-native-rsa-native';
2
import { NativeModules } from 'react-native';
3
import type { Aes as AesType } from 'react-native-aes-crypto';
4
import { encode as btoa } from 'base-64';
5
import { toByteArray, fromByteArray } from 'base64-js';
6
import * as sss from './secrets.js';
7
8
const Aes: any = NativeModules.Aes;
9
const EPHEMERAL_KEY_SIZE = 16;
10
const BITS = '8';
11
12
export async function split(
13
secret: string,
14
numShare: number,
15
threshold: number,
16
): Promise<string[]> {
17
return (await sss.split(secret, numShare, threshold)).map(share => share.substring(1));
18
}
19
20
export function combine(shares: string[]): string {
21
return sss.combine(shares.map(share => BITS + share));
22
}
23
24
export interface SymCiphertext {
25
ciphertext: string;
26
iv: string;
27
}
28
29
export interface HybridCiphertext {
30
encryptedMessage: SymCiphertext;
31
encryptedEphemeralKey: string;
32
}
33
34
export function randomBytes(length: number): Promise<string> {
35
return Aes.randomKey(length);
36
}
37
38
function generateRsaKeyPair(keySize: number = 4096): Promise<KeyPair> {
39
return RSA.generateKeys(keySize);
40
}
41
42
export function generateEncryptionKeyPair(
43
options: { type: 'rsa'; keySize: number } = { type: 'rsa', keySize: 4096 },
44
): Promise<KeyPair> {
45
if (options.type === 'rsa') {
46
return generateRsaKeyPair(options.keySize);
47
}
48
throw new Error(`Unsupported type ${options.type}`);
49
}
50
51
export function generateSigningKeyPair(
52
options: { type: 'rsa'; keySize: number } = { type: 'rsa', keySize: 4096 },
53
): Promise<KeyPair> {
54
return generateEncryptionKeyPair(options);
55
}
56
57
export async function encryptSym(
58
data: string,
59
symKey: string,
60
algorithm: AesType.Algorithms = 'aes-128-cbc',
61
) {
62
const iv = await Aes.randomKey(16);
63
const ciphertext = await Aes.encrypt(data, symKey, iv, algorithm);
64
return {
65
ciphertext,
66
iv: hexToBase64(iv),
67
};
68
}
69
70
export function decryptSym(
71
ciphertext: SymCiphertext,
72
symKey: string,
73
algorithm: AesType.Algorithms = 'aes-128-cbc',
74
): Promise<string> {
75
const iv = base64ToHex(ciphertext.iv);
76
return Aes.decrypt(ciphertext.ciphertext, symKey, iv, algorithm);
77
}
78
79
export async function encryptAsym(
80
data: string,
81
encKey: string,
82
): Promise<string | HybridCiphertext> {
83
try {
84
return await RSA.encrypt64(data, encKey);
85
} catch (err: unknown) {
86
// if error indicates plaintext too big
87
// avoid infinite recursive calls
88
if (data.length <= EPHEMERAL_KEY_SIZE) {
89
throw new Error(
90
'Ephemeral key size is too large for the encryption key type',
91
);
92
}
93
return encryptHybrid(data, encKey);
94
}
95
}
96
97
export function decryptAsym(
98
ciphertext: string | HybridCiphertext,
99
decKey: string,
100
): Promise<string> {
101
if (typeof ciphertext === 'string') {
102
return RSA.decrypt64(ciphertext, decKey);
103
}
104
return decryptHybrid(ciphertext, decKey);
105
}
106
107
export async function encryptHybrid(
108
data: string,
109
encKey: string,
110
): Promise<HybridCiphertext> {
111
const ephemeralKey = await Aes.randomKey(EPHEMERAL_KEY_SIZE);
112
const encryptedMessage = await encryptSym(data, ephemeralKey);
113
const encryptedEphemeralKey = await encryptAsym(ephemeralKey, encKey);
114
if (typeof encryptedEphemeralKey !== 'string') {
115
throw new Error(
116
'Ephemeral key size is too large for the encryption key type',
117
);
118
}
119
// encryptedEphemeralKey must be returned by crypto.publicEncrypt, i.e. a Uint8Array
120
return { encryptedMessage, encryptedEphemeralKey };
121
}
122
123
export async function decryptHybrid(
124
ciphertext: HybridCiphertext,
125
decKey: string,
126
): Promise<string> {
127
const { encryptedEphemeralKey } = ciphertext;
128
const ephemeralKey = await decryptAsym(encryptedEphemeralKey, decKey);
129
const { encryptedMessage } = ciphertext;
130
return decryptSym(encryptedMessage, ephemeralKey);
131
}
132
133
export function sign(data: string, signingKey: string): Promise<string> {
134
return RSA.sign(data, signingKey);
135
}
136
137
export function verify(
138
data: string,
139
verificationKey: string,
140
signature: string,
141
): Promise<boolean> {
142
return RSA.verify(signature, data, verificationKey);
143
}
144
145
export function hash(...data: string[]): Promise<string> {
146
return Aes.sha256(data.join(''));
147
}
148
149
export function xor(a: string, b: string): string {
150
const bChunks = split2(b);
151
return split2(a)
152
.map((byteHex, i) =>
153
// eslint-disable-next-line no-bitwise
154
(parseInt(byteHex, 16) ^ parseInt(bChunks[i], 16))
155
.toString(16)
156
.padStart(2, '0'),
157
)
158
.join('');
159
}
160
161
function split2(str: string): string[] {
162
const chunks = str.match(/\w{2}/g);
163
if (chunks === null) {
164
throw new Error('Invalid encoding');
165
}
166
return chunks;
167
}
168
169
export function hexToBase64(hex: string): string {
170
const chunks = split2(hex);
171
return btoa(
172
chunks.map(byteHex => String.fromCharCode(parseInt(byteHex, 16))).join(''),
173
);
174
}
175
176
export function base64ToHex(b64: string): string {
177
return binToHex(toByteArray(b64));
178
}
179
180
export function hexToBin(hex: string): Uint8Array {
181
const chunks = split2(hex);
182
return Uint8Array.from(chunks.map(byteHex => parseInt(byteHex, 16)));
183
}
184
185
export function binToHex(bin: Uint8Array): string {
186
return bin.reduce(
187
(str, byte) => str + byte.toString(16).padStart(2, '0'),
188
'',
189
);
190
}
191
192
export { toByteArray as base64ToBin, fromByteArray as BinToBase64 };
193
194
export function hexToUtf8(hex: string): string {
195
return sss.hex2str(hex);
196
}
197
198
export function utf8ToHex(a: string): string {
199
return sss.str2hex(a);
200
}
201
202
export function binToUtf8(bin: Uint8Array): string {
203
return String.fromCharCode(...bin);
204
}
205
206
export function utf8ToBin(a: string): Uint8Array {
207
return Uint8Array.from(a.split('').map(byte => byte.charCodeAt(0)));
208
}
209
210
export function base64ToUtf8(b64: string): string {
211
return binToUtf8(toByteArray(b64));
212
}
213
214
export function utf8ToBase64(a: string): string {
215
return fromByteArray(utf8ToBin(a));
216
}
Copied!
  • EPHEMERAL_KEY_SIZE (global variable): is the number of random bytes of the ephemeral keys used in a hybrid (symmetric & asymmetric) encryption.
  • randomBytes (function): generates random bytes encoded in hexidecimal.
  • generateEncryptionKeyPair (function): generates fresh encryption key pairs.
  • generateSigningKeyPair (function): generates fresh signing key pairs.
  • encryptSym (function): performs a symmetric encryption. Algorithm: AES-128-CBC.
  • decryptSym (function): performs a symmetric decryption. Algorithm: AES-128-CBC
  • encryptAsym (function): performs an asymmetric encryption. If the data to be encrypted is too large, it uses a hybrid encryption instead. Algorithm: RSA
  • decryptAsym (function): performs an asymmetric decryption. If the ciphertext is a result from a hybrid encryption, then it uses hybrid decryption; Otherwise, it uses asymmetric decryption. Algorithm: RSA
  • encryptHybrid (function): performs a hybrid encryption: performs a symmetric encryption on the data with an ephemeral key, then performs an asymmetric encryption on the ephemeral key with the encryption key.
  • decryptHybrid (function): performs a hybrid decryption: retrieve the ephemeral key with an asymmetric decryption. Then it performs a symmetric decryption on the payload with the ephemeral key.
  • sign (function): digitally signs a piece of data with a signing key. Algorithm: depends on input key type; data is first hashed with SHA512;
  • verify (function): verifies if a signature matches with the alleged data and verification key. Algorithm: depends on input key type; data is first hashed with SHA512.
  • hash (function): hashes the data in the argument list. Algorithm: SHA256
  • xor (function): performs exclusive-or on two binary arrays
  • hexToBase64 (function): transforms the encoding of binary data from hexidecimal to base64
  • base64ToHex (function): transforms the encoding of binary data from base64 to hexidecimal
  • hexToBin (function): decodes a hexidecimal-encoded string to a Uint8Array
  • binToHex (function): encodes a Uint8Array to hexidecimal
  • base64ToBin (function): decodes a base64-encoded string to a Uint8Array
  • binToBase64 (function): encodes a Uint8Array to base64

Encrypting

An Encryption is a JSON object that has the following form:
1
{
2
"description": string,
3
"tokenHash": 256-bit-hex-string,
4
"ciphertext": hybrid-ciphertext,
5
"trusteeShares": {
6
[trusteeId]: {
7
"encrypted": base64-string,
8
"hashed": 256-bit-hex-string,
9
},
10
},
11
"validatorShares": {
12
[validatorId]: {
13
"encrypted": hybrid-ciphertext,
14
},
15
},
16
}
Copied!
, where hybrid-ciphertext has the form
1
{
2
"encryptedMessage": {
3
"ciphertext": base64-string,
4
"iv": base64-string,
5
},
6
"encryptedEphemeralKey": base64-string,
7
}
Copied!
The encryption phase involves the encryptor sending an Encryption to the PAD server. Thus, it is essential to understand how it is created. We will go through its properties one by one.
  • "description": Description of the encryption. This can be an arbitrary string.
  • "tokenHash": Hased value of a token. tokenHash = SHA256(token).
  • "ciphertext": The ciphertext of the secret encrypted with the decryptor's public key and the symmetric key k.
  • "trusteeShares": A dictionary containing all the encrypted and hashed trustee shares.
    • [trusteeId]: A dictionary entry where the key is a trustee's ID, and the value is an object of encrypted and hashed trustee shares.
      • "encrypted": The trustee's share of the masked symmetric key k \oplus R used to encrypt the secret, encoded as a base64 string.
      • "hashed": SHA256 of the trustee's share, encoded as a hex string.
  • "validatorShares": A dictionary containing all the encrypted validator shares.
    • [validatorId]: A dictionary entry where the key is a validator's ID, and the value is an object of encrypted validator shares.
      • "encrypted": The validator's share of the mask R to the symmetric key used to encrypt the secret together with the decryptor's public key. Since the public key is large, the validator's shares are large too. Thus, the encryption of a validator's share uses an ephemeral key as a symmetric key. k and decryptor's key.

Creating token and tokenHash

A token is a random number sent from the encryptor to the decryptor for her to later request for a piece of encryptor's data. It is essential to keep it secret between the encryptor and decryptor until the data request phase. An encryption's ID is tokenHash, the hash value of token. For portability and readability, both token and tokenHash are represented as hexadecimal strings.
Important: note that token should be taken as a lowercase string when hashed into tokenHash.
1
// create-token-and-hash.js
2
import {randomBytes, hash} from './cryptoUtil';
3
4
const token: string = await randomBytes(16); // 'd1fbe8b5f3ffd7a16a2aa400ebc0194f'
5
const tokenHash: string = await hash(token); // '5892a6c7ad71b358df760b4291a8adf974f77bd9d3f16c4fd38c58147e80f401'
Copied!

Creating ciphertext

The ciphertext part of an encryption should be encrypted with the symmetric key first, then the decryptor's encryption key. To ensure integrity, a digital signature from the encryptor against the ciphertext in the first encryption should be attached before the second encryption. In general, the first step creates the ciphertext
c=SymEnck(token,s)c = SymEnc_{k}(token, s)
Then the second step creates
AsymEncBek(c,SignAsk(c))AsymEnc_{Bek}(c, Sign_{Ask}(c))
For example, suppose secret = "my_secret" is the encryptor's secret, the following code snippet generates c.
1
// create-ciphertext-first-step.js
2
import {randomBytes, encryptSym} from './cryptoUtil';
3
4
const token: string = /* from create-token-and-hash.js */;
5
const secret: string = 'my_secret';
6
const dataJson = {token, secret};
7
const data: string = JSON.stringify(dataJson);
8
9
const k: string = await randomBytes(16);
10
const c: string = await encryptSym(data, k);
Copied!
With c, the ciphertext can be created like this:
1
// create-ciphertext-second-step.js
2
import {sign, encryptHybrid, SymCiphertext, HybridCiphertext} from './cryptoUtil';
3
4
const c: SymCiphertext = /* from create-ciphertext-first-step.js */;
5
const encryptorSigningKey: string = /* from some keystore */;
6
const decryptorEncryptionKey: string = /* from some keystore */;
7
const payload: string = JSON.stringify(c);
8
const signature: string = await crypto.sign(payload, encryptorSigningKey);
9
const signedPayload: string = JSON.stringify({payload, signature});
10
11
const ciphertext: HybridCiphertext = await encryptHybrid(signedPayload, decryptorEncryptionKey);
Copied!

Creating trusteeShares

To create trusteeShares, the encryptor should first have knowledge about the settings of the instance, including the trustee threshold and the list of trustees referencing the instance (check out GET /metadata and GET /all-trustees/{trustee-id}. These information are also in one of the first few blocks on the ledger). The symmetric key k is then masked with a random number. This masked symmetric key is the secret shared among the trustees. After that, each share of trustees' is encrypted with their individual encryption key (the mapping between secret sharing index and trustee does not matter, but we recommend following the order in the instance's metadata. For example, trustee1 may hold a share of index 00 in an encryption, but hold another share of index 01 in another encryption). For auditing purposes, the hash of each share before encrypting should also be included, so that after a trustee post her share, consistency can be checked.
1
import {
2
split,
3
hexToBase64,
4
hexToUtf8,
5
encryptAsym,
6
isHybridCiphertext,
7
hash,
8
Shares } from './cryptoUtil';
9
10
type TrusteeShares = {
11
[trusteeId: string]: {
12
encrypted: string,
13
hashed: string,
14
},
15
};
16
17
async function createTrusteeShares(maskedSymKey: string, trustees: Object, trusteeThreshold: number): Promise<TrusteeShares> {
18
const n = Object.keys(trustees).length;
19
const shares: Shares = await split(maskedSymKey, n, trusteeThreshold);
20
const trusteeShares: TrusteeShares = {};
21
for (const [i, [trusteeId, {encryptionKey}]] of Object.entries(trustees).entries()) {
22
const shareBase64 = hexToBase64(shares[i]);
23
const shareUtf8 = hexToUtf8(shares[i]);
24
const encryptedShare = await encryptAsym(shareBase64, encryptionKey);
25
if (isHybridCiphertext(encryptedShare)) {
26
throw new Error('share is too large');
27
}
28
const hashedShare = await hash(shareUtf8);
29
trusteeShares[trusteeId] = {
30
encrypted: encryptedShare,
31
hashed: hashedShare,
32
};
33
}
34
return trusteeShares;
35
}
36
37
const k = /* from create-ciphertext-first-step.js */;
38
const R = crypto.randomBytes(16);
39
40
// the masked symmetric key maskedK is the secret to be shared
41
const maskedK = xor(k, R);
42
43
const trustees = {
44
'trustee1': {
45
encryptionKey: /* trustee1's encryption key */,
46
},
47
'trustee2': {
48
encryptionKey: /* trustee2's encryption key */,
49
},
50
};
51
const trusteeThreshold = 2;
52
const trusteeShares = await createTrusteeShares(maskedK, trustees, trusteeThreshold);
Copied!

Creating validatorShares

This is similar to creating trusteeShares, except the secret shared among the validators is the mask R together with the decryptor's public key to identify him.
1
import {
2
split,
3
hexToBase64,
4
hexToUtf8,
5
encryptHybrid,
6
utf8ToHex,
7
hash,
8
Shares } from './cryptoUtil';
9
10
type ValidatorShares = {
11
[validatorId: string]: {
12
encrypted: string,
13
},
14
};
15
16
async function createValidatorShares(RAndBvkPayload: string, validators: Object, validatorThreshold: number): Promise<ValidatorShares> {
17
const nPrime = Object.keys(validators).length;
18
const shares: Shares = await split(RAndBvkPayload, nPrime, validatorThreshold);
19
const validatorShares: ValidatorShares = {};
20
for (const [i, [validatorId, {encryptionKey}]] of Object.entries(validators).entries()) {
21
const shareBase64 = hexToBase64(shares[i]);
22
const encryptedShare = await encryptHybrid(shareBase64, encryptionKey);
23
validatorShares[validatorId] = {
24
encrypted: encryptedShare,
25
};
26
}
27
return validatorShares;
28
}
29
30
const R = /* from create-trustee-shares.js */;
31
const bvk = /* decryptor's verification key or identity */;
32
33
const RAndBvkJson = {
34
mask: hexToBase64(R),
35
decryptorIdentity: bvk,
36
};
37
const RAndBvkPayload = utf8ToHex(JSON.stringify(RAndBvkJson));
38
39
const validators = {
40
'validator1': {
41
encryptionKey: /* validator1's encryption key */,
42
},
43
'validator2': {
44
encryptionKey: /* validator2's encryption key */,
45
},
46
};
47
const validatorThreshold = 2;
48
49
const validatorShares = await createValidatorShares(RAndBvkPayload, validators, validatorThreshold);
Copied!

Creating channelKey

You will also need a signing key pair for creating the new channel. This ensures that only the encryptor can modify the Encryption object using endpoint for updating an Encryption in a channel.
This channel key pair should be generated freshly and must not be the key pair that identifies the encryptor. Only the public (verification) key is sent to the server. The encryptor should keep the private (signing) key so long as she would update the Encryption therein.
1
// generate-channel-key.js
2
import {generateSigningKeyPair} from './cryptoUtil';
3
4
const {private: channelSigningKey, public: channelKey} = await generateSigningKeyPair();
Copied!
That's it! We have gone through the steps of creating a new channel. Recall that the encryption is being sent to the PAD service server and the token is then shared with the decryptor out-of-band. The encryption channel allows the encryptor to update the secret which is useful in some use cases.

Updating Encryption object

Using the channel signing key and the token-hash, the encryptor can update her Encryption object in the channel associated with the token. The following code snippets show how one create encryptionPayload and signature required for the operation.
1
import {sign} from './cryptoUtil';
2
3
const channelSigningKey = /* from generate-channel-key.js */
4
const newEncryption = {
5
// the new encryption content...
6
};
7
8
const encryptionPayload = JSON.stringify(newEncryption);
9
const signature = await sign(encryptionPayload, channelSigningKey);
Copied!

Decrypting

The decryption phase happens after the encryption has been uploaded, a data request has been posted, and sufficient number of trustees and validators, respectivelly, have responded. At this stage, the decryptor has enough information to decrypt the encryptor's secret.

Verifying responses correctness

It is important for the decryptor to check correctness and integrity of the trustee and validator responses. Checking correctness can be done by checking consistency between a response and its hash submitted by the encryptor at encryption time. Checking integrity involves verifying a signature against the response payload.
1
/* verify-responses.js */
2
// Work in progress
Copied!

Verifying ciphertext integrity

Recall that the ciphertext payload includes the encryptor's signature before encrypted with decryptor's encryption key. This ensures that the payload is not modified by third party, including the PAD server. The signature needs to be verified before decrypting.
1
/* verify-ciphertext.js */
2
import {decryptHybrid, verify, HybridCiphertext, SymCiphertext} from './cryptoUtil';
3
4
function decryptVerifyCiphertext(ciphertext: HybridCiphertext, decryptorDecryptionKey: string, encryptorVerificationKey: string): SymCiphertext {
5
const decrypted = decryptHybrid(ciphertext, decryptorDecryptionKey);
6
const {payload, signature} = JSON.parse(decrypted);
7
if (!verify(payload, encryptorVerificationKey, signature)) {
8
throw new Error('Signature not match with payload');
9
}
10
const innerCiphertext = JSON.parse(payload);
11
return innerCiphertext;
12
}
13
14
const {ciphertext} = /* from GET /encryptions/{token-hash}/ciphertext */;
15
const decryptorDecryptionKey = /* from some key store */;
16
const encryptorVerificationKey = /* from some key store */;
17
18
const c = decryptVerifyCiphertext(
19
ciphertext,
20
decryptorDecryptionKey,
21
encryptorVerificationKey,
22
);
Copied!

Reconstructing masked symmetric key from trustees

To perform the symmetric decryption we need sufficient responses from trustees and validators, respectively. Then those will combine to the masked symmetric key and the masked. We show how to reconstruct the masked symmetric key from trustees' responses in this section. Obviously, it has redundancy with our previous example. In actual implementation, these scripts can be merged. For example, parse the response, push the share to an array only if it is valid, then combine those later.
1
/* reconstruct-masked-k.js */
2
import {base64ToHex, combine} from './cryptoUtil';
3
4
const {trusteeResponses} = /* from GET /data-requests/{token}/trustee-responses */;
5
const trusteeShares = Object.values(trusteeResponses).map((signed) => {
6
const trusteeResponse = JSON.parse(signed.trusteeResponse);
7
const share = base64ToHex(trusteeResponse.trusteeShare);
8
return share;
9
});
10
const maskedK = combine(trusteeShares);
Copied!

Reconstructing the mask from validators

1
/* resconstruct-r.js */
2
import {base64ToHex, hexToUtf8, combine} from './cryptoUtil';
3
4
const {validatorResponses} = /* from GET /data-requests/{token}/validator-responses */;
5
const validatorShares = Object.values(validatorResponses).map((signed) => {
6
const validatorResponse = JSON.parse(signed.validatorResponse);
7
const share = base64ToHex(validatorResponse.validatorShare);
8
return share;
9
});
10
const RAndBvkPayload = combine(validatorShares);
11
const RAndBvkString = hexToUtf8(RAndBvkPayload)();
12
const RAndBvkJson = JSON.parse(RAndBvkString);
13
const R = base64ToHex(RAndBvkJson.mask);
Copied!

Decrypting!

The decryptor now has everything to retrieve the encryptor's secret.
1
import {xor, decryptSym} from './cryptoUtil';
2
3
const maskedK = /* from reconstruct-masked-k.js */;
4
const R = /* from reconstruct-r.js */;
5
const c = /* from verify-ciphertext.js */;
6
7
const k = xor(maskedK, R);
8
const decrypted = decryptSym(c, k);
9
const dataJson = JSON.parse(decrypted);
10
const {token, secret} = dataJson;
11
if (token !== /* token */) {
12
throw new Error('Token mismatch');
13
}
14
console.log(secret); // my_secret
Copied!