Skip to content

Commit

Permalink
Replace node-rsa with webcrypto api (removes the need for polyfills i…
Browse files Browse the repository at this point in the history
…n browser) (#101)

* downgrade node-rsa

* v0.12.5-alpha.0

* upgrade node-rsa again

* v0.12.5-alpha.1

* fix: use better assert import

* v0.12.5-alpha.2

* change import again

* v0.12.5-alpha.3

* comment out asserts for now

* v0.12.5-alpha.4

* checkout webcrypto api, get trusted op could not be deciphered error

* [worker] wip with webcrypto api

* v0.12.5-alpha.5

* [worker] fix local import

* v0.12.5-alpha.6

* [worker] fix accessing crypto

* v0.12.5-alpha.7

* [worker] correctly print encrypted stuff

* v0.12.5-alpha.8

* [worker] add some doc

* [worker] better doc for cryptoProvider

* [worker/webCryptoRSA] fix logging pubKey

* [worker] test all endianness in unit tests

* Revert "[worker] test all endianness in unit tests"

This reverts commit c8f69f1.

* [worker] add swapEndianness function, which doesn't work

* Revert "[worker] add swapEndianness function, which doesn't work"

This reverts commit 0f56377.

* Revert "Revert "[worker] test all endianness in unit tests""

This reverts commit b6ee1c4.

* [worker] consistent endianness in key import

* [worker] fix: await encryption promise

* [worker] working encryptions for a local setup!

* [worker] improve efficiency of bit-endianness conversion

* [worker] remove tests for byte-endianness as they are unneeded

* v0.12.5-alpha.9

* [worker] switch to local-docker network for tests

* add webcrypto notes to readme

* ignore tests that need a running setup

* [worker] remove unnecessary endinanness function args from interface

* [worker] cleanup

* [worker] export  to fix unused warning
  • Loading branch information
clangenb authored May 2, 2024
1 parent 00fc7e7 commit 36dd5f4
Show file tree
Hide file tree
Showing 31 changed files with 238 additions and 151 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed .yarn/cache/asn1-npm-0.2.4-219dd49411-aa5d6f77b1.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Encointer JavaScript API monorepo

# Installation

## Crypto library
The worker uses the webcrypto api if it is run in the browser. This library is only
defined if you access the webpage with `localhost` in firefox. It is not available
on `127.0.0.1` or `0.0.0.0` due to browser security policies.


```bash
yarn add @encointer/node-api @encointer/worker-api
```
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"publishConfig": {
"directory": "build"
},
"version": "0.12.4"
"version": "0.12.5-alpha.9"
}
4 changes: 2 additions & 2 deletions packages/node-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
},
"sideEffects": false,
"type": "module",
"version": "0.12.4",
"version": "0.12.5-alpha.9",
"main": "index.js",
"dependencies": {
"@encointer/types": "^0.12.4",
"@encointer/types": "^0.12.5-alpha.9",
"@polkadot/api": "^10.9.1",
"tslib": "^2.5.3"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"sideEffects": false,
"type": "module",
"version": "0.12.4",
"version": "0.12.5-alpha.9",
"main": "index.js",
"scripts": {
"generate:defs": "node --experimental-specifier-resolution=node --loader ts-node/esm ../../node_modules/.bin/polkadot-types-from-defs --package @encointer/types/interfaces --input ./src/interfaces",
Expand Down
2 changes: 1 addition & 1 deletion packages/util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"sideEffects": false,
"type": "module",
"types": "./index.d.ts",
"version": "0.12.4",
"version": "0.12.5-alpha.9",
"main": "index.js",
"dependencies": {
"@babel/runtime": "^7.18.9",
Expand Down
4 changes: 2 additions & 2 deletions packages/util/src/assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { parseDegree } from "@encointer/types";
import {u64, Vec} from "@polkadot/types";
import {Option} from "@polkadot/types-codec";
import type {Moment} from "@polkadot/types/interfaces/runtime";
import assert from "assert";
// import assert from "assert";

/**
* Performs the same meetup assignment as the encointer-ceremonies pallet.
Expand Down Expand Up @@ -234,7 +234,7 @@ export function assignmentFnInverse(
}

// never observed in practice
assert(t3 >= 0, `[assignment_fn_inverse]: t3 smaller 0: ${t3}`);
// assert(t3 >= 0, `[assignment_fn_inverse]: t3 smaller 0: ${t3}`);

participants.push(t3)

Expand Down
23 changes: 11 additions & 12 deletions packages/util/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import assert from 'assert';
import type { KeyringPair } from "@polkadot/keyring/types";
import { Keyring } from "@polkadot/keyring";
import BN from "bn.js";

interface assertLengthFunc {
(upper: number, lower: number): number
}

export const assertLength: assertLengthFunc = function (upper, lower) {
const len = upper + lower;
assert(len >= 8, `Bit length can't be less than 8, provided ${len}`);
assert(len <= 128, `Bit length can't be bigger than 128, provided ${len}`);
assert(!(len & (len - 1)), `Bit length should be power of 2, provided ${len}`);
return len;
};
// interface assertLengthFunc {
// (upper: number, lower: number): number
// }
//
// export const assertLength: assertLengthFunc = function (upper, lower) {
// const len = upper + lower;
// assert(len >= 8, `Bit length can't be less than 8, provided ${len}`);
// assert(len <= 128, `Bit length can't be bigger than 128, provided ${len}`);
// assert(!(len & (len - 1)), `Bit length should be power of 2, provided ${len}`);
// return len;
// };

export interface PubKeyPinPair {
pubKey: string,
Expand Down
9 changes: 5 additions & 4 deletions packages/util/src/parserFixPoint.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import BN from "bn.js";
import assert from "assert";
// import assert from "assert";

import { assertLength } from "./common.js";
// import { assertLength } from "./common.js";

export interface ParserFixPointFn {
(raw: BN, precision?: number): number;
Expand All @@ -25,9 +25,10 @@ export interface ParserFixPointFactory {
/// raw: substrate_fixed::types::I<upper>F<lower> as I<upper+lower>
/// precision: 0..lower number bits in fractional part to process
export const parserFixPoint: ParserFixPointFactory = function (upper, lower) {
const len = assertLength(upper, lower);
// const len = assertLength(upper, lower);
const len = upper + lower;
return (raw: BN, precision: number = lower): number => {
assert(raw.bitLength() <= len, "Bit length is not equal to " + len);
// assert(raw.bitLength() <= len, "Bit length is not equal to " + len);

raw = raw.fromTwos(len);

Expand Down
11 changes: 5 additions & 6 deletions packages/util/src/toFixPoint.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import assert from 'assert';
import BN from 'bn.js';

import {assertLength, fractionalToRadix2, safeIntegerToRadix2} from './common.js';
import {fractionalToRadix2, safeIntegerToRadix2} from './common.js';

export interface ToFixPointFn {
(num: number): BN;
Expand All @@ -20,7 +19,7 @@ export interface StringToFixPointFactory {
}

export const toFixPoint: ToFixPointFactory = function (upper, lower) {
assertLength(upper, lower);
// assertLength(upper, lower);
return (num: number): BN => {
const [upperBits, lowerBits] = num.toString(2).split('.');

Expand All @@ -29,7 +28,7 @@ export const toFixPoint: ToFixPointFactory = function (upper, lower) {
};

export const stringToFixPoint: StringToFixPointFactory = function (upper, lower) {
assertLength(upper, lower);
// assertLength(upper, lower);
return (num: string): BN => {
let [integers, fractions] = num.split('.');

Expand All @@ -52,9 +51,9 @@ export const stringToFixPoint: StringToFixPointFactory = function (upper, lower)
* @param fractions_count amount of fractional bits in the fixed-point type.
*/
const toFixed = function(integers: string, fractions: string, integer_count: number, fractions_count: number): BN {
assertLength(integer_count, fractions_count);
// assertLength(integer_count, fractions_count);

assert(integers.length <= integer_count, 'Number is larger than maximum in '.concat(integer_count.toString(), 'bit'));
// assert(integers.length <= integer_count, 'Number is larger than maximum in '.concat(integer_count.toString(), 'bit'));

if (fractions !== undefined) {
const bits = integers.concat(fractions.length > fractions_count ? fractions.substring(0, fractions_count) : fractions.padEnd(fractions_count, '0'));
Expand Down
10 changes: 5 additions & 5 deletions packages/worker-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
"sideEffects": false,
"type": "module",
"types": "./index.d.ts",
"version": "0.12.4",
"version": "0.12.5-alpha.9",
"main": "index.js",
"dependencies": {
"@encointer/node-api": "^0.12.4",
"@encointer/types": "^0.12.4",
"@encointer/util": "^0.12.4",
"@learntheropes/node-rsa": "^1.1.3",
"@encointer/node-api": "^0.12.5-alpha.9",
"@encointer/types": "^0.12.5-alpha.9",
"@encointer/util": "^0.12.5-alpha.9",
"@peculiar/webcrypto": "^1.4.6",
"@polkadot/api": "^10.9.1",
"@polkadot/keyring": "^12.3.2",
"@polkadot/types": "^10.9.1",
Expand Down
3 changes: 0 additions & 3 deletions packages/worker-api/src/encointerWorker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type {u32, u64, Vec} from '@polkadot/types';
import {communityIdentifierFromString} from '@encointer/util';

// @ts-ignore
import NodeRSA from '@learntheropes/node-rsa';

import type {
CommunityIdentifier,
MeetupIndexType,
Expand Down
3 changes: 2 additions & 1 deletion packages/worker-api/src/integriteeWorker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,14 @@ describe('worker', () => {
describe('balance unshield should work', () => {
it('should return value', async () => {
const shard = network.chosenCid;

const result = await worker.balanceUnshieldFunds(
alice,
shard,
network.mrenclave,
alice.address,
charlie.address,
1100000000000
1100000000000,
);
console.log('balance unshield result', result.toHuman());
expect(result).toBeDefined();
Expand Down
4 changes: 0 additions & 4 deletions packages/worker-api/src/integriteeWorker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type {u32} from '@polkadot/types';

// @ts-ignore
import NodeRSA from '@learntheropes/node-rsa';


import type {KeyringPair} from '@polkadot/keyring/types';
import type {Balance, Hash} from '@polkadot/types/interfaces/runtime';
import type {
Expand Down
6 changes: 3 additions & 3 deletions packages/worker-api/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface IWorker extends WebSocketAsPromised {
keyring: () => Keyring | undefined;
createType: (apiType: string, obj?: any) => any;
open: () => Promise<Event>;
encrypt: (data: Uint8Array) => Vec<u8>
encrypt: (data: Uint8Array) => Promise<Vec<u8>>
registry: () => TypeRegistry
}

Expand Down Expand Up @@ -49,8 +49,8 @@ export interface PublicGetterArgs {
export type RequestArgs = PublicGetterArgs | TrustedGetterArgs | { }

export interface CallOptions {
timeout: number;
debug: boolean;
timeout?: number;
debug?: boolean;
}

export enum Request {
Expand Down
52 changes: 4 additions & 48 deletions packages/worker-api/src/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { parseI64F64 } from '@encointer/util';
import { u8aToBn } from '@polkadot/util';
import {parseI64F64} from '@encointer/util';
import {u8aToBn} from '@polkadot/util';

// @ts-ignore
import NodeRSA from '@learntheropes/node-rsa';

import type { IWorker } from './interface.js';
import type { BalanceEntry } from "@encointer/types";
import BN from "bn.js";
import type {IWorker} from './interface.js';
import type {BalanceEntry} from "@encointer/types";

export function parseBalance(self: IWorker, data: any): BalanceEntry {
const balanceEntry = self.createType('BalanceEntry<BlockNumber>', data);
Expand All @@ -23,44 +19,4 @@ export function parseBalanceType(data: any): number {
return parseI64F64(u8aToBn(data));
}

/**
* Parse a public key retrieved from the worker into `NodeRsa`.
*
* Note: This code is relatively sensitive: Changes here could lead
* to errors parsing and encryption errors in the browser, probably
* because of inconsistencies of node's `Buffer and the `buffer`
* polyfill in browser.
* @param data
*/
export function parseNodeRSA(data: any): NodeRSA {
const keyJson = JSON.parse(data);
keyJson.n = new BN(keyJson.n, 'le');
keyJson.e = new BN(keyJson.e);
const key = new NodeRSA();
setKeyOpts(key);
key.importKey({
// Important: use string here, not buffer, otherwise the browser will
// misinterpret the `n`.
n: keyJson.n.toString(10),
// Important: use number here, not buffer, otherwise the browser will
// misinterpret the `e`.
e: keyJson.e.toNumber()
}, 'components-public');
return key;
}

function setKeyOpts(key: NodeRSA) {
key.setOptions(
{
// Enforce using the pure javascript implementations by
// setting the `browser` environment, as compatibility
// with node's crypto is broken and leads to bad outputs.
environment: 'browser',
encryptionScheme: {
scheme: 'pkcs1_oaep',
hash: 'sha256',
label: ''
}
}
);
}
2 changes: 1 addition & 1 deletion packages/worker-api/src/sendRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const sendTrustedCall = async <T>(self: IWorker, call: IntegriteeTrustedC

console.log(`TrustedOperation: ${JSON.stringify(top)}`);

const cyphertext = self.encrypt(top.toU8a());
const cyphertext = await self.encrypt(top.toU8a());

const r = self.createType(
'Request', { shard, cyphertext: cyphertext }
Expand Down
4 changes: 2 additions & 2 deletions packages/worker-api/src/testUtils/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export const localDockerNetwork = () => {
chain: 'ws://127.0.0.1:9944',
worker: 'wss://127.0.0.1:2000',
genesisHash: '0x388c446a804e24e77ae89f5bb099edb60cacc2ac7c898ce175bdaa08629c1439',
mrenclave: 'HjkQuPjBn531Hkji2Dsj4CEYCGpqCc3aXqETMCM7x7z4',
chosenCid: 'HjkQuPjBn531Hkji2Dsj4CEYCGpqCc3aXqETMCM7x7z4',
mrenclave: '9jm9Wm4DwGxsUUPA1cvcWWxyTuynpJ2YeEcNGnm8nztk',
chosenCid: '9jm9Wm4DwGxsUUPA1cvcWWxyTuynpJ2YeEcNGnm8nztk',
customTypes: {},
palletOverrides: {}
};
Expand Down
76 changes: 76 additions & 0 deletions packages/worker-api/src/webCryptoRSA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import BN from "bn.js";

/**
* Provides crypto the browser via the native crypto, and in the node-js environment (like our tests)
* via the `@peculiar/webcrypto` polyfill.
*/
let cryptoProvider: any;

if (typeof window !== "undefined" && typeof window.crypto !== "undefined") {
cryptoProvider = window.crypto;
} else {
const { Crypto } = require("@peculiar/webcrypto");
cryptoProvider = new Crypto();
}

/**
* Type depending on our environment browser vs. node-js.
*/
type CryptoKey = import("crypto").KeyObject | import("@peculiar/webcrypto").CryptoKey;


export async function parseWebCryptoRSA(data: any): Promise<CryptoKey> {
const keyJson = JSON.parse(data);

// Convert Base64url-encoded components to ArrayBuffer
const nArrayBuffer = new Uint8Array(new BN(keyJson.n, 'le').toArray());
const eArrayBuffer = new Uint8Array(new BN(keyJson.e, 'le').toArray());

// Import the components into CryptoKey
const publicKey = await cryptoProvider.subtle.importKey(
"jwk",
{
kty: "RSA",
e: uint8ArrayToBase64Url(eArrayBuffer),
n: uint8ArrayToBase64Url(nArrayBuffer),
ext: true,
},
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
["encrypt"]
);

console.log(`PublicKey: ${JSON.stringify(publicKey)}`);

return publicKey;
}

export async function encryptWithPublicKey(data: Uint8Array, publicKey: CryptoKey): Promise<ArrayBuffer> {
const encryptedData = await cryptoProvider.subtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
data
);

// console.log(`EncryptedData: ${JSON.stringify({encrypted: buf2hex(encryptedData)})}`);

return encryptedData;
}


function uint8ArrayToBase64Url(uint8Array: Uint8Array): string {
const base64String = btoa(String.fromCharCode(...uint8Array));
return base64String
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}

export function buf2hex(buffer: ArrayBuffer) { // buffer is an ArrayBuffer
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
Loading

0 comments on commit 36dd5f4

Please sign in to comment.