Merge branch 'cic-meta-builds' into 'master'
Cic meta builds See merge request grassrootseconomics/cic-internal-integration!13
This commit is contained in:
		
						commit
						42674e00ff
					
				@ -4,6 +4,7 @@ include:
 | 
			
		||||
  - local: 'apps/cic-eth/.gitlab-ci.yml'
 | 
			
		||||
  - local: 'apps/cic-ussd/.gitlab-ci.yml'
 | 
			
		||||
  - local: 'apps/cic-notify/.gitlab-ci.yml'
 | 
			
		||||
  - local: 'apps/cic-meta/.gitlab-ci.yml'
 | 
			
		||||
 | 
			
		||||
stages:
 | 
			
		||||
  - build
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Subproject commit 76e8b80965ccedbc59790248050fa598a747f85a
 | 
			
		||||
							
								
								
									
										14
									
								
								apps/cic-meta/.config/database.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/cic-meta/.config/database.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
[database]
 | 
			
		||||
#name = cic-meta
 | 
			
		||||
#engine = postgres
 | 
			
		||||
#user = postgres
 | 
			
		||||
#password = password
 | 
			
		||||
#host = localhost
 | 
			
		||||
#port = 5432
 | 
			
		||||
name = /tmp/cicmeta.sqlite
 | 
			
		||||
engine = sqlite
 | 
			
		||||
user = 
 | 
			
		||||
password = 
 | 
			
		||||
host = 
 | 
			
		||||
port = 
 | 
			
		||||
schema_sql_path = server.sqlite.sql
 | 
			
		||||
							
								
								
									
										7
									
								
								apps/cic-meta/.config/pgp.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								apps/cic-meta/.config/pgp.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
[pgp]
 | 
			
		||||
exports_dir = pgp
 | 
			
		||||
privatekey_file = privatekeys.asc
 | 
			
		||||
passphrase = merman
 | 
			
		||||
publickey_trusted_file = publickeys.asc
 | 
			
		||||
publickey_active_file = publickeys.asc
 | 
			
		||||
publickey_encrypt_file = publickeys.asc
 | 
			
		||||
							
								
								
									
										3
									
								
								apps/cic-meta/.config/server.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/cic-meta/.config/server.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
[server]
 | 
			
		||||
address = 0.0.0.0
 | 
			
		||||
port = 7777
 | 
			
		||||
							
								
								
									
										5
									
								
								apps/cic-meta/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/cic-meta/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
dist-web
 | 
			
		||||
scratch
 | 
			
		||||
tests
 | 
			
		||||
							
								
								
									
										24
									
								
								apps/cic-meta/.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/cic-meta/.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
 | 
			
		||||
.cic_meta_variables:
 | 
			
		||||
    variables:
 | 
			
		||||
        APP_NAME: cic-meta
 | 
			
		||||
        DOCKERFILE_PATH: $APP_NAME/docker/Dockerfile
 | 
			
		||||
 | 
			
		||||
.this_changes_target:
 | 
			
		||||
    rules:
 | 
			
		||||
        - changes:
 | 
			
		||||
            - $CONTEXT/$APP_NAME/*
 | 
			
		||||
 | 
			
		||||
build-mr-cic-meta:
 | 
			
		||||
    extends:
 | 
			
		||||
        - .this_changes_target
 | 
			
		||||
        - .py_build_merge_request
 | 
			
		||||
        - .cic_meta_variables
 | 
			
		||||
 | 
			
		||||
build-push-cic-meta:
 | 
			
		||||
    extends:
 | 
			
		||||
        - .this_changes_target
 | 
			
		||||
        - .py_build_push
 | 
			
		||||
        - .cic_meta_variables
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								apps/cic-meta/CHANGELOG
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								apps/cic-meta/CHANGELOG
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
* 0.0.6
 | 
			
		||||
	- Add server build
 | 
			
		||||
* 0.0.5
 | 
			
		||||
	- Set build on install
 | 
			
		||||
* 0.0.4
 | 
			
		||||
	- Change phone key generator to arbitrary value input
 | 
			
		||||
* 0.0.3
 | 
			
		||||
	- Add asset key generator
 | 
			
		||||
	- Add crypto polyfill (node uses native crypto, web uses webcrypto)
 | 
			
		||||
	- Add phone asset
 | 
			
		||||
							
								
								
									
										28
									
								
								apps/cic-meta/docker/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								apps/cic-meta/docker/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
FROM node:15.3.0-alpine3.10
 | 
			
		||||
 | 
			
		||||
WORKDIR /tmp/src/cic-meta
 | 
			
		||||
 | 
			
		||||
COPY cic-meta/package.json \
 | 
			
		||||
	 cic-meta/package-lock.json \
 | 
			
		||||
     ./
 | 
			
		||||
 | 
			
		||||
RUN npm install
 | 
			
		||||
 | 
			
		||||
COPY cic-meta/src/ src/
 | 
			
		||||
COPY cic-meta/tests/ tests/
 | 
			
		||||
COPY cic-meta/scripts/ scripts/
 | 
			
		||||
#COPY docker/*.sh /root/
 | 
			
		||||
 | 
			
		||||
RUN alias tsc=node_modules/typescript/bin/tsc 
 | 
			
		||||
 | 
			
		||||
COPY cic-meta/.config/ /usr/local/etc/cic-meta/
 | 
			
		||||
COPY cic-meta/scripts/server/server.postgres.sql /usr/local/share/cic-meta/sql/server.sql
 | 
			
		||||
 | 
			
		||||
COPY cic-meta/docker/db.sh ./db.sh
 | 
			
		||||
RUN chmod 755 ./db.sh
 | 
			
		||||
 | 
			
		||||
RUN alias ts-node=/tmp/src/cic-meta/node_modules/ts-node/dist/bin.js
 | 
			
		||||
ENTRYPOINT [ "./node_modules/ts-node/dist/bin.js", "./scripts/server/server.ts" ]
 | 
			
		||||
 | 
			
		||||
# COPY cic-meta/docker/start_server.sh ./start_server.sh
 | 
			
		||||
# RUN chmod 755 ./start_server.sh
 | 
			
		||||
							
								
								
									
										3
									
								
								apps/cic-meta/docker/db.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/cic-meta/docker/db.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
PGPASSWORD=$DATABASE_PASSWORD psql -U $DATABASE_USER -h $DATABASE_HOST -p $DATABASE_PORT -d $DATABASE_NAME /usr/local/share/cic-meta/sql/server.sql
 | 
			
		||||
							
								
								
									
										3
									
								
								apps/cic-meta/docker/start_server.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/cic-meta/docker/start_server.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
sh ./db.sh
 | 
			
		||||
 | 
			
		||||
/usr/local/bin/node /usr/local/bin/cic-meta-server $@
 | 
			
		||||
							
								
								
									
										98
									
								
								apps/cic-meta/example/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								apps/cic-meta/example/client.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
import sys
 | 
			
		||||
import os
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from urllib.request import Request, urlopen
 | 
			
		||||
 | 
			
		||||
import gnupg
 | 
			
		||||
 | 
			
		||||
logging.basicConfig(level=logging.DEBUG)
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
host = os.environ.get('CIC_META_URL', 'http://localhost:63380')
 | 
			
		||||
 | 
			
		||||
if len(sys.argv) < 2:
 | 
			
		||||
    sys.stderr.write('Usage: {} <path-to-gpg-private-key>\n'.format(sys.argv[0]))
 | 
			
		||||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Import PGP key used to sign the data submission
 | 
			
		||||
gpg = gnupg.GPG(gnupghome='/tmp/.gpg')
 | 
			
		||||
f = open(sys.argv[1], 'r')
 | 
			
		||||
key_data = f.read()
 | 
			
		||||
f.close()
 | 
			
		||||
 | 
			
		||||
gpg.import_keys(key_data)
 | 
			
		||||
gpgk = gpg.list_keys()
 | 
			
		||||
algo = gpgk[0]['algo']
 | 
			
		||||
logg.info('using signing key {} algo {}'.format(gpgk[0]['keyid'], algo))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
 | 
			
		||||
    # Random key to associate with value
 | 
			
		||||
    # (typically this is some deterministic identifier like sha256(<ethaddress>:cic-person)
 | 
			
		||||
    k = os.urandom(32).hex()
 | 
			
		||||
    url = os.path.join(host, k)
 | 
			
		||||
 | 
			
		||||
    # Headers required for server-assisted merge operations
 | 
			
		||||
    headers = {
 | 
			
		||||
        'X-CIC-AUTOMERGE': 'server',
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    # Data to merge
 | 
			
		||||
    data_dict = {
 | 
			
		||||
        'foo': 'bar',
 | 
			
		||||
        'xyzzy': 42,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    # Send request to server to get initial automerge object and signing material
 | 
			
		||||
    # Server will reply with current state of object merged with ours, but (obviously)
 | 
			
		||||
    # still without a signature.
 | 
			
		||||
    data = json.dumps(data_dict).encode('utf-8')
 | 
			
		||||
    req = Request(url, headers=headers, data=data, method='POST')
 | 
			
		||||
    rs = urlopen(req)
 | 
			
		||||
    logg.info('get sign material response status: {}'.format(rs.status))
 | 
			
		||||
    if rs.status != 200:
 | 
			
		||||
        raise RuntimeError('request failed: {}'.format(rs.reason))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # Sign the provided digest
 | 
			
		||||
    data = rs.read()
 | 
			
		||||
    e = json.loads(data)
 | 
			
		||||
    sig = gpg.sign(e['digest'], passphrase='ge', keyid=gpgk[0]['keyid'])
 | 
			
		||||
 | 
			
		||||
    # Format data for the content storage request
 | 
			
		||||
    data = {
 | 
			
		||||
            'm': data.decode('utf-8'),
 | 
			
		||||
            's': {
 | 
			
		||||
        'engine': 'pgp',
 | 
			
		||||
        'algo': algo,
 | 
			
		||||
        'data': str(sig),
 | 
			
		||||
        'digest': e['digest'],
 | 
			
		||||
            },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    # Send storage request to server
 | 
			
		||||
    data = json.dumps(data).encode('utf-8')
 | 
			
		||||
    req = Request(url, headers=headers, data=data, method='PUT')
 | 
			
		||||
    rs = urlopen(req)
 | 
			
		||||
 | 
			
		||||
    logg.info('signed content submissionstatus: {}'.format(rs.status))
 | 
			
		||||
    if rs.status != 200:
 | 
			
		||||
        raise RuntimeError('request failed: {}'.format(rs.reason))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # Get the latest stored version of the data (without the merge graph)
 | 
			
		||||
    req = Request(url, method='GET')
 | 
			
		||||
    rs = urlopen(req)
 | 
			
		||||
    logg.info('get latest data status: {}'.format(rs.status))
 | 
			
		||||
    if rs.status != 200:
 | 
			
		||||
        raise RuntimeError('request failed: {}'.format(rs.reason))
 | 
			
		||||
 | 
			
		||||
    print(rs.read().decode('utf-8'))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										3309
									
								
								apps/cic-meta/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3309
									
								
								apps/cic-meta/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										41
									
								
								apps/cic-meta/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								apps/cic-meta/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "cic-client-meta",
 | 
			
		||||
	"version": "0.0.6",
 | 
			
		||||
	"description": "Signed CRDT metadata graphs for the CIC network",
 | 
			
		||||
	"main": "dist/index.js",
 | 
			
		||||
	"types": "dist/index.d.ts",
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"test": "mocha -r node_modules/node-localstorage/register -r ts-node/register tests/*.ts",
 | 
			
		||||
		"build": "node_modules/typescript/bin/tsc -d --outDir dist",
 | 
			
		||||
		"build-server": "tsc -d --outDir dist-server scripts/server/*.ts",
 | 
			
		||||
		"pack": "node_modules/typescript/bin/tsc -d --outDir dist && webpack",
 | 
			
		||||
		"clean": "rm -rf dist"
 | 
			
		||||
	},
 | 
			
		||||
	"bin": {
 | 
			
		||||
		"cic-meta-server": "./dist-server/scripts/server/server.js"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@ethereumjs/tx": "^3.0.0-beta.1",
 | 
			
		||||
		"automerge": "^0.14.1",
 | 
			
		||||
		"ethereumjs-wallet": "^1.0.1",
 | 
			
		||||
		"ini": "^1.3.5",
 | 
			
		||||
		"openpgp": "^4.10.8",
 | 
			
		||||
		"pg": "^8.4.2",
 | 
			
		||||
		"sqlite3": "^5.0.0",
 | 
			
		||||
		"yargs": "^16.1.0"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@types/mocha": "^8.0.3",
 | 
			
		||||
		"mocha": "^8.2.0",
 | 
			
		||||
		"node-localstorage": "^2.1.6",
 | 
			
		||||
		"ts-node": "^9.0.0",
 | 
			
		||||
		"typescript": "^4.0.5",
 | 
			
		||||
		"webpack": "^5.4.0",
 | 
			
		||||
		"webpack-cli": "^4.2.0"
 | 
			
		||||
	},
 | 
			
		||||
	"author": "Louis Holbrook <dev@holbrook.no>",
 | 
			
		||||
	"license": "GPL-3.0-or-later",
 | 
			
		||||
	"engines": {
 | 
			
		||||
		"node": "~15.3.0"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								apps/cic-meta/scripts/dumpconfig.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/cic-meta/scripts/dumpconfig.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
const config = require('./src/config');
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
 | 
			
		||||
if (process.argv[2] === undefined) {
 | 
			
		||||
	process.stderr.write('Usage: node dumpConfig.js <configdir>\n');
 | 
			
		||||
	process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
try {
 | 
			
		||||
	const stat = fs.statSync(process.argv[2]);
 | 
			
		||||
	if (!stat.isDirectory()) {
 | 
			
		||||
		throw 'not a directory';
 | 
			
		||||
	}
 | 
			
		||||
} catch {
 | 
			
		||||
	process.stderr.write('Not a directory: ' + process.argv[2] + '\n');
 | 
			
		||||
	process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const c = new config.Config(process.argv[2], process.env['CONFINI_ENV_PREFIX']);
 | 
			
		||||
c.process();
 | 
			
		||||
process.stdout.write(c.toString());
 | 
			
		||||
							
								
								
									
										28
									
								
								apps/cic-meta/scripts/server/args.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								apps/cic-meta/scripts/server/args.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
const args = require('yargs');
 | 
			
		||||
 | 
			
		||||
const standardArgs = args.option('config', {
 | 
			
		||||
	alias: 'c',
 | 
			
		||||
	type: 'string',
 | 
			
		||||
	description: 'absolute path to configuation files directory', 
 | 
			
		||||
 | 
			
		||||
}).option('env-prefix', {
 | 
			
		||||
	type: 'string',
 | 
			
		||||
	description: 'prefix to add to environment variables to match configuration directives',
 | 
			
		||||
 | 
			
		||||
}).option('database-engine', {
 | 
			
		||||
	type: 'string',
 | 
			
		||||
	description: 'database engines to use', 
 | 
			
		||||
 | 
			
		||||
}).option('address', {
 | 
			
		||||
	alias: 'a',
 | 
			
		||||
	type: 'string',
 | 
			
		||||
	description: 'ip address to bind server to',
 | 
			
		||||
 | 
			
		||||
}).option('server-address', {
 | 
			
		||||
	alias: 'p',
 | 
			
		||||
	type: 'number',
 | 
			
		||||
	description: 'port to bind server to',
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export { standardArgs };
 | 
			
		||||
							
								
								
									
										211
									
								
								apps/cic-meta/scripts/server/handlers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								apps/cic-meta/scripts/server/handlers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,211 @@
 | 
			
		||||
import * as Automerge from 'automerge';
 | 
			
		||||
import * as pgp from 'openpgp';
 | 
			
		||||
import * as pg from 'pg';
 | 
			
		||||
 | 
			
		||||
import { Envelope, Syncable } from '../../src/sync';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function handleNoMergeGet(db, digest, keystore) {
 | 
			
		||||
	const sql = "SELECT content FROM store WHERE hash = '" + digest + "'";
 | 
			
		||||
	return new Promise<string|boolean>((whohoo, doh) => {
 | 
			
		||||
		db.query(sql, (e, rs) => {
 | 
			
		||||
			if (e !== null && e !== undefined) {
 | 
			
		||||
				doh(e);
 | 
			
		||||
				return;
 | 
			
		||||
			} else if (rs.rowCount == 0) {
 | 
			
		||||
				whohoo(false);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const cipherText = rs.rows[0]['content'];
 | 
			
		||||
			pgp.message.readArmored(cipherText).then((m) => {
 | 
			
		||||
				const opts = {
 | 
			
		||||
					message: m,
 | 
			
		||||
					privateKeys: [keystore.getPrivateKey()],
 | 
			
		||||
				};
 | 
			
		||||
				pgp.decrypt(opts).then((plainText) => {
 | 
			
		||||
					const o = Syncable.fromJSON(plainText.data);
 | 
			
		||||
					const r = JSON.stringify(o.m['data']);
 | 
			
		||||
					whohoo(r);
 | 
			
		||||
				}).catch((e) => {
 | 
			
		||||
					console.error('decrypt', e);
 | 
			
		||||
					doh(e);
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e) => {
 | 
			
		||||
				console.error('mesage', e);
 | 
			
		||||
				doh(e);
 | 
			
		||||
			});
 | 
			
		||||
		})
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: add input for change description
 | 
			
		||||
function handleServerMergePost(data, db, digest, keystore, signer) {
 | 
			
		||||
	return new Promise<string>((whohoo, doh) => {
 | 
			
		||||
		const o = JSON.parse(data);
 | 
			
		||||
		const cipherText = handleClientMergeGet(db, digest, keystore).then(async (v) => {
 | 
			
		||||
			let e = undefined;
 | 
			
		||||
			let s = undefined;
 | 
			
		||||
			if (v === undefined) {
 | 
			
		||||
				s = new Syncable(digest, data);
 | 
			
		||||
				s.onwrap = (e) => {
 | 
			
		||||
					whohoo(e.toJSON());
 | 
			
		||||
				};
 | 
			
		||||
				digest = s.digest();
 | 
			
		||||
				s.wrap({
 | 
			
		||||
					digest: digest,
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				e = Envelope.fromJSON(v);
 | 
			
		||||
				s = e.unwrap();
 | 
			
		||||
				s.replace(o, 'server merge');
 | 
			
		||||
				e.set(s);
 | 
			
		||||
				s.onwrap = (e) => {
 | 
			
		||||
					whohoo(e.toJSON());
 | 
			
		||||
				}
 | 
			
		||||
				digest = s.digest();
 | 
			
		||||
				s.wrap({
 | 
			
		||||
					digest: digest,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: this still needs to merge with the stored version
 | 
			
		||||
function handleServerMergePut(data, db, digest, keystore, signer) {
 | 
			
		||||
	return new Promise<boolean>((whohoo, doh) => {
 | 
			
		||||
		const wrappedData = JSON.parse(data);
 | 
			
		||||
 | 
			
		||||
		if (wrappedData.s === undefined) {
 | 
			
		||||
			doh('signature missing');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const e = Envelope.fromJSON(wrappedData.m);
 | 
			
		||||
		let s = undefined;
 | 
			
		||||
		try {
 | 
			
		||||
			s = e.unwrap();
 | 
			
		||||
		} catch(e) {
 | 
			
		||||
			console.error(e)
 | 
			
		||||
			whohoo(undefined);
 | 
			
		||||
		}
 | 
			
		||||
		// TODO: we probably should expose method for replacing the signature, this is too intrusive
 | 
			
		||||
		s.m = Automerge.change(s.m, 'sign', (doc) => {
 | 
			
		||||
			doc['signature'] = wrappedData.s;
 | 
			
		||||
		});
 | 
			
		||||
		s.setSigner(signer);
 | 
			
		||||
		s.onauthenticate = (v) => {
 | 
			
		||||
			console.log('vvv', v);
 | 
			
		||||
			if (!v) {
 | 
			
		||||
				whohoo(undefined);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			const opts = {
 | 
			
		||||
				message: pgp.message.fromText(s.toJSON()),
 | 
			
		||||
				publicKeys: keystore.getEncryptKeys(),
 | 
			
		||||
			};
 | 
			
		||||
			pgp.encrypt(opts).then((cipherText) => {
 | 
			
		||||
				const sql = "INSERT INTO store (owner_fingerprint, hash, content) VALUES ('" + signer.fingerprint() + "', '" + digest + "', '" + cipherText.data + "') ON CONFLICT (hash) DO UPDATE SET content = EXCLUDED.content;";
 | 
			
		||||
				db.query(sql, (e, rs) => {
 | 
			
		||||
					if (e !== null && e !== undefined) {
 | 
			
		||||
						doh(e);
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					whohoo(true);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		};
 | 
			
		||||
		s.authenticate(true)
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function handleClientMergeGet(db, digest, keystore) {
 | 
			
		||||
	const sql = "SELECT content FROM store WHERE hash = '" + digest + "'";
 | 
			
		||||
	return new Promise<string>((whohoo, doh) => {
 | 
			
		||||
		db.query(sql, (e, rs) => {
 | 
			
		||||
			console.log('rs', e, rs);
 | 
			
		||||
			if (e !== null && e !== undefined) {
 | 
			
		||||
				doh(e);
 | 
			
		||||
				return;
 | 
			
		||||
			} else if (rs.rowCount == 0) { // TODO fix the postgres/sqlite method name issues, this will now break on postgres
 | 
			
		||||
				whohoo(undefined);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			const cipherText = rs.rows[0]['content'];
 | 
			
		||||
			pgp.message.readArmored(cipherText).then((m) => {
 | 
			
		||||
				const opts = {
 | 
			
		||||
					message: m,
 | 
			
		||||
					privateKeys: [keystore.getPrivateKey()],
 | 
			
		||||
				};
 | 
			
		||||
				pgp.decrypt(opts).then((plainText) => {
 | 
			
		||||
					const o = Syncable.fromJSON(plainText.data);
 | 
			
		||||
					const e = new Envelope(o);
 | 
			
		||||
					whohoo(e.toJSON());
 | 
			
		||||
				}).catch((e) => {
 | 
			
		||||
					console.error('decrypt', e);
 | 
			
		||||
					doh(e);
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e) => {
 | 
			
		||||
				console.error('mesage', e);
 | 
			
		||||
				doh(e);
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: this still needs to merge with the stored version
 | 
			
		||||
function handleClientMergePut(data, db, digest, keystore, signer) {
 | 
			
		||||
	return new Promise<boolean>((whohoo, doh) => {
 | 
			
		||||
		let s = undefined;
 | 
			
		||||
		try {
 | 
			
		||||
			const e = Envelope.fromJSON(data);
 | 
			
		||||
			s = e.unwrap();
 | 
			
		||||
		} catch(e) {
 | 
			
		||||
			whohoo(false);
 | 
			
		||||
			console.error(e)
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s.setSigner(signer);
 | 
			
		||||
		s.onauthenticate = (v) => {
 | 
			
		||||
			if (!v) {
 | 
			
		||||
				whohoo(false);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			handleClientMergeGet(db, digest, keystore).then((v) => {
 | 
			
		||||
				if (v !== undefined) {
 | 
			
		||||
					const env = Envelope.fromJSON(v);
 | 
			
		||||
					s.merge(env.unwrap());
 | 
			
		||||
				}
 | 
			
		||||
				const opts = {
 | 
			
		||||
					message: pgp.message.fromText(s.toJSON()),
 | 
			
		||||
					publicKeys: keystore.getEncryptKeys(),
 | 
			
		||||
				};
 | 
			
		||||
				pgp.encrypt(opts).then((cipherText) => {
 | 
			
		||||
					const sql = "INSERT INTO store (owner_fingerprint, hash, content) VALUES ('" + signer.fingerprint() + "', '" + digest + "', '" + cipherText.data + "') ON CONFLICT (hash) DO UPDATE SET content = EXCLUDED.content;";
 | 
			
		||||
					db.query(sql, (e, rs) => {
 | 
			
		||||
						if (e !== null && e !== undefined) {
 | 
			
		||||
							doh(e);
 | 
			
		||||
							return;
 | 
			
		||||
						}
 | 
			
		||||
						whohoo(true);
 | 
			
		||||
					});
 | 
			
		||||
				}).catch((e) => {
 | 
			
		||||
					doh(e);	
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		};
 | 
			
		||||
		s.authenticate(true)
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	handleClientMergePut,
 | 
			
		||||
	handleClientMergeGet,
 | 
			
		||||
	handleServerMergePost,
 | 
			
		||||
	handleServerMergePut,
 | 
			
		||||
	handleNoMergeGet,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										8
									
								
								apps/cic-meta/scripts/server/server.postgres.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								apps/cic-meta/scripts/server/server.postgres.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
create table if not exists store (
 | 
			
		||||
	id serial primary key not null,
 | 
			
		||||
	owner_fingerprint text not null,
 | 
			
		||||
	hash char(64) not null unique,
 | 
			
		||||
	content text not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
create index if not exists idx_fp on store ((lower(owner_fingerprint)));
 | 
			
		||||
							
								
								
									
										9
									
								
								apps/cic-meta/scripts/server/server.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/cic-meta/scripts/server/server.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
create table if not exists store (
 | 
			
		||||
	/*id serial primary key not null,*/
 | 
			
		||||
	id integer primary key autoincrement,
 | 
			
		||||
	owner_fingerprint text not null,
 | 
			
		||||
	hash char(64) not null unique,
 | 
			
		||||
	content text not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
create index if not exists idx_fp on store ((lower(owner_fingerprint)));
 | 
			
		||||
							
								
								
									
										9
									
								
								apps/cic-meta/scripts/server/server.sqlite.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/cic-meta/scripts/server/server.sqlite.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
create table if not exists store (
 | 
			
		||||
	/*id serial primary key not null,*/
 | 
			
		||||
	id integer primary key autoincrement,
 | 
			
		||||
	owner_fingerprint text not null,
 | 
			
		||||
	hash char(64) not null unique,
 | 
			
		||||
	content text not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
create index if not exists idx_fp on store ((lower(owner_fingerprint)));
 | 
			
		||||
							
								
								
									
										207
									
								
								apps/cic-meta/scripts/server/server.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										207
									
								
								apps/cic-meta/scripts/server/server.ts
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,207 @@
 | 
			
		||||
import * as http from 'http';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import * as pgp from 'openpgp';
 | 
			
		||||
 | 
			
		||||
import * as handlers from './handlers';
 | 
			
		||||
import { Envelope, Syncable } from '../../src/sync';
 | 
			
		||||
import { PGPKeyStore, PGPSigner } from '../../src/auth';
 | 
			
		||||
 | 
			
		||||
import { standardArgs } from './args';
 | 
			
		||||
import { Config } from '../../src/config';
 | 
			
		||||
import { SqliteAdapter, PostgresAdapter } from '../../src/db';
 | 
			
		||||
 | 
			
		||||
let configPath = '/usr/local/etc/cic-meta';
 | 
			
		||||
 | 
			
		||||
const argv = standardArgs.argv;
 | 
			
		||||
 | 
			
		||||
if (argv['config'] !== undefined) {
 | 
			
		||||
	configPath = argv['config'];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const config = new Config(configPath, argv['env-prefix']);
 | 
			
		||||
config.process();
 | 
			
		||||
console.debug(config.toString());
 | 
			
		||||
 | 
			
		||||
let fp = path.join(config.get('PGP_EXPORTS_DIR'), config.get('PGP_PUBLICKEY_ACTIVE_FILE'));
 | 
			
		||||
const pubksa = fs.readFileSync(fp, 'utf-8');
 | 
			
		||||
fp = path.join(config.get('PGP_EXPORTS_DIR'), config.get('PGP_PRIVATEKEY_FILE'));
 | 
			
		||||
const pksa = fs.readFileSync(fp, 'utf-8');
 | 
			
		||||
 | 
			
		||||
const dbConfig = {
 | 
			
		||||
	'name': config.get('DATABASE_NAME'),
 | 
			
		||||
	'user': config.get('DATABASE_USER'),
 | 
			
		||||
	'password': config.get('DATABASE_PASSWORD'),
 | 
			
		||||
	'host': config.get('DATABASE_HOST'),
 | 
			
		||||
	'port': config.get('DATABASE_PORT'),
 | 
			
		||||
	'engine': config.get('DATABASE_ENGINE'),
 | 
			
		||||
};
 | 
			
		||||
let db = undefined;
 | 
			
		||||
if (config.get('DATABASE_ENGINE') == 'sqlite') {
 | 
			
		||||
       	db = new SqliteAdapter(dbConfig);
 | 
			
		||||
} else if (config.get('DATABASE_ENGINE') == 'postgres')  {
 | 
			
		||||
       	db = new PostgresAdapter(dbConfig);
 | 
			
		||||
} else {
 | 
			
		||||
	throw 'database engine ' + config.get('DATABASE_ENGINE') + 'not implemented';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
let signer = undefined;
 | 
			
		||||
const keystore = new PGPKeyStore(config.get('PGP_PASSPHRASE'), pksa, pubksa, pubksa, pubksa, () => {
 | 
			
		||||
	keysLoaded();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function keysLoaded() {
 | 
			
		||||
	signer = new PGPSigner(keystore);
 | 
			
		||||
	prepareServer();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function migrateDatabase(cb) {
 | 
			
		||||
	try {
 | 
			
		||||
		const sql = "SELECT 1 FROM store;"
 | 
			
		||||
		db.query(sql, (e, rs) => {
 | 
			
		||||
			if (e === null || e === undefined) {
 | 
			
		||||
				cb();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			console.warn('db check for table "store" fail', e);
 | 
			
		||||
			
 | 
			
		||||
			console.debug('using schema path', config.get('DATABASE_SCHEMA_SQL_PATH'));
 | 
			
		||||
			const sql = fs.readFileSync(config.get('DATABASE_SCHEMA_SQL_PATH'), 'utf-8');
 | 
			
		||||
			db.query(sql, (e, rs) => {
 | 
			
		||||
				if (e !== undefined && e !== null) {
 | 
			
		||||
					console.error('db initialization fail', e);
 | 
			
		||||
					return;
 | 
			
		||||
				} 
 | 
			
		||||
				cb();
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		});
 | 
			
		||||
	} catch(e) {
 | 
			
		||||
		console.warn('table store does not exist', e);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function prepareServer() {
 | 
			
		||||
	await migrateDatabase(startServer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function startServer() {
 | 
			
		||||
	http.createServer(processRequest).listen(config.get('SERVER_PORT'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const re_digest = /^\/([a-fA-F0-9]{64})\/?$/;
 | 
			
		||||
function parseDigest(url) {
 | 
			
		||||
	const digest_test = url.match(re_digest);
 | 
			
		||||
	if (digest_test === null) {
 | 
			
		||||
		throw 'invalid digest';	
 | 
			
		||||
	}
 | 
			
		||||
	return digest_test[1].toLowerCase();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function processRequest(req, res) {
 | 
			
		||||
	let digest = undefined;
 | 
			
		||||
 | 
			
		||||
	if (!['PUT', 'GET', 'POST'].includes(req.method)) {
 | 
			
		||||
		res.writeHead(405, {"Content-Type": "text/plain"});
 | 
			
		||||
		res.end();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		digest = parseDigest(req.url);
 | 
			
		||||
	} catch(e) {
 | 
			
		||||
		res.writeHead(400, {"Content-Type": "text/plain"});
 | 
			
		||||
		res.end();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const mergeHeader = req.headers['x-cic-automerge'];
 | 
			
		||||
	let mod = req.method.toLowerCase() + ":automerge:";
 | 
			
		||||
	switch (mergeHeader) {
 | 
			
		||||
		case "client":
 | 
			
		||||
			mod += "client"; // client handles merges
 | 
			
		||||
			break;
 | 
			
		||||
		case "server":
 | 
			
		||||
			mod += "server"; // server handles merges
 | 
			
		||||
			break;
 | 
			
		||||
		default:
 | 
			
		||||
			mod += "none"; // merged object only (get only)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let data = '';
 | 
			
		||||
	req.on('data', (d) => {
 | 
			
		||||
		data += d;
 | 
			
		||||
	});
 | 
			
		||||
	req.on('end', async () => {
 | 
			
		||||
		console.debug('mode', mod);
 | 
			
		||||
		let content = '';
 | 
			
		||||
		let contentType = 'application/json';
 | 
			
		||||
		let r:any = undefined;
 | 
			
		||||
		try {
 | 
			
		||||
			switch (mod) {
 | 
			
		||||
				case 'put:automerge:client':
 | 
			
		||||
					r = await handlers.handleClientMergePut(data, db, digest, keystore, signer);
 | 
			
		||||
					if (r == false) {
 | 
			
		||||
						res.writeHead(403, {"Content-Type": "text/plain"});
 | 
			
		||||
						res.end();
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				case 'get:automerge:client':
 | 
			
		||||
					content = await handlers.handleClientMergeGet(db, digest, keystore);	
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				case 'post:automerge:server':
 | 
			
		||||
					content = await handlers.handleServerMergePost(data, db, digest, keystore, signer);	
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				case 'put:automerge:server':
 | 
			
		||||
					r = await handlers.handleServerMergePut(data, db, digest, keystore, signer);	
 | 
			
		||||
					if (r == false) {
 | 
			
		||||
						res.writeHead(403, {"Content-Type": "text/plain"});
 | 
			
		||||
						res.end();
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					break;
 | 
			
		||||
				//case 'get:automerge:server':
 | 
			
		||||
				//	content = await handlers.handleServerMergeGet(db, digest, keystore);	
 | 
			
		||||
				//	break;
 | 
			
		||||
 | 
			
		||||
				case 'get:automerge:none':
 | 
			
		||||
					r = await handlers.handleNoMergeGet(db, digest, keystore);	
 | 
			
		||||
					if (r == false) {
 | 
			
		||||
						res.writeHead(404, {"Content-Type": "text/plain"});
 | 
			
		||||
						res.end();
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					content = r;
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				default:
 | 
			
		||||
					res.writeHead(400, {"Content-Type": "text/plain"});
 | 
			
		||||
					res.end();
 | 
			
		||||
					return;
 | 
			
		||||
			}
 | 
			
		||||
		} catch(e) {
 | 
			
		||||
			console.error('fail', mod, digest, e);
 | 
			
		||||
			res.writeHead(500, {"Content-Type": "text/plain"});
 | 
			
		||||
			res.end();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (content === undefined) {
 | 
			
		||||
			res.writeHead(400, {"Content-Type": "text/plain"});
 | 
			
		||||
			res.end();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res.writeHead(200, {
 | 
			
		||||
			"Content-Type": contentType,
 | 
			
		||||
			"Content-Length": content.length,
 | 
			
		||||
		});
 | 
			
		||||
		res.write(content);
 | 
			
		||||
		res.end();
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								apps/cic-meta/src/assets/phone.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/cic-meta/src/assets/phone.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
import { ArgPair, Syncable } from '../sync';
 | 
			
		||||
import { Addressable, addressToBytes, bytesToHex, toKey } from '../digest';
 | 
			
		||||
 | 
			
		||||
class Phone extends Syncable implements Addressable {
 | 
			
		||||
 | 
			
		||||
	address:	string
 | 
			
		||||
	value:		number
 | 
			
		||||
 | 
			
		||||
	constructor(address:string, v:number) {
 | 
			
		||||
		const o = {
 | 
			
		||||
			msisdn: v,
 | 
			
		||||
		}
 | 
			
		||||
		super('', o);
 | 
			
		||||
		Phone.toKey(v).then((phid) => {
 | 
			
		||||
			this.id = phid;	
 | 
			
		||||
			this.address = address;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static async toKey(msisdn:number) {
 | 
			
		||||
		return await toKey(msisdn.toString(), ':cic.msisdn');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public key(): string {
 | 
			
		||||
		return this.id;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	Phone,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								apps/cic-meta/src/assets/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								apps/cic-meta/src/assets/user.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
import { ArgPair, Syncable } from '../sync';
 | 
			
		||||
import { Addressable, addressToBytes, bytesToHex, toAddressKey } from '../digest';
 | 
			
		||||
 | 
			
		||||
const keySalt = new TextEncoder().encode(':cic.person');
 | 
			
		||||
class User extends Syncable implements Addressable {
 | 
			
		||||
 | 
			
		||||
	address:	string
 | 
			
		||||
	firstName: 	string
 | 
			
		||||
	lastName:	string
 | 
			
		||||
 | 
			
		||||
	constructor(address:string, v:Object={}) {
 | 
			
		||||
		if (v['user'] === undefined) {
 | 
			
		||||
			v['user'] = {
 | 
			
		||||
				firstName: '',
 | 
			
		||||
				lastName: '',
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		User.toKey(address).then((uid) => {
 | 
			
		||||
			this.id = uid;
 | 
			
		||||
			this.address = address;
 | 
			
		||||
		});
 | 
			
		||||
		super('', v);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static async toKey(address:string) {
 | 
			
		||||
		return await toAddressKey(address, ':cic.person');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public key(): string {
 | 
			
		||||
		return this.id;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public setName(firstName:string, lastName:string) {
 | 
			
		||||
		//const fn = new ArgPair('user.firstName', firstName)
 | 
			
		||||
		//const ln = new ArgPair('user.lastName', lastName)
 | 
			
		||||
		const n = {
 | 
			
		||||
			'user': {
 | 
			
		||||
				'firstName': firstName,
 | 
			
		||||
				'lastName': lastName,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		//this.update([fn, ln], 'update name');
 | 
			
		||||
		this.replace(n, 'update name');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { User };
 | 
			
		||||
							
								
								
									
										191
									
								
								apps/cic-meta/src/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								apps/cic-meta/src/auth.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
			
		||||
import * as pgp from 'openpgp';
 | 
			
		||||
import * as crypto from 'crypto';
 | 
			
		||||
 | 
			
		||||
interface Signable {
 | 
			
		||||
	digest():string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type KeyGetter = () => any;
 | 
			
		||||
 | 
			
		||||
type Signature = {
 | 
			
		||||
	engine:string
 | 
			
		||||
	algo:string
 | 
			
		||||
	data:string
 | 
			
		||||
	digest:string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Signer { 
 | 
			
		||||
	prepare(Signable):boolean;
 | 
			
		||||
	onsign(Signature):void;
 | 
			
		||||
	onverify(boolean):void;
 | 
			
		||||
	sign(digest:string):void
 | 
			
		||||
	verify(digest:string, signature:Signature):void
 | 
			
		||||
	fingerprint():string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Authoritative {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface KeyStore {
 | 
			
		||||
	getPrivateKey:		KeyGetter
 | 
			
		||||
	getFingerprint:		() => string
 | 
			
		||||
	getTrustedKeys: 	() => Array<any>
 | 
			
		||||
	getTrustedActiveKeys: 	() => Array<any>
 | 
			
		||||
	getEncryptKeys: 	() => Array<any>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PGPKeyStore implements KeyStore {
 | 
			
		||||
 | 
			
		||||
	fingerprint:	string
 | 
			
		||||
	pk:		any
 | 
			
		||||
 | 
			
		||||
	pubk = {
 | 
			
		||||
		active: [],
 | 
			
		||||
		trusted: [],
 | 
			
		||||
		encrypt: [],
 | 
			
		||||
	}
 | 
			
		||||
	loads = 0x00;
 | 
			
		||||
	loadsTarget = 0x0f;
 | 
			
		||||
	onload: (k:KeyStore) => void;
 | 
			
		||||
 | 
			
		||||
	constructor(passphrase:string, pkArmor:string, pubkActiveArmor:string, pubkTrustedArmor:string, pubkEncryptArmor:string, onload = (ks:KeyStore) => {}) {
 | 
			
		||||
		this._readKey(pkArmor, undefined, 1, passphrase);
 | 
			
		||||
		this._readKey(pubkActiveArmor, 'active', 2);
 | 
			
		||||
		this._readKey(pubkTrustedArmor, 'trusted', 4);
 | 
			
		||||
		this._readKey(pubkEncryptArmor, 'encrypt', 8);
 | 
			
		||||
		this.onload = onload;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private _readKey(a:string, x:any, n:number, pass?:string) {
 | 
			
		||||
		pgp.key.readArmored(a).then((k) => {
 | 
			
		||||
			if (pass !== undefined) {
 | 
			
		||||
				this.pk = k.keys[0];
 | 
			
		||||
				this.pk.decrypt(pass).then(() => {
 | 
			
		||||
					this.fingerprint = this.pk.getFingerprint();
 | 
			
		||||
					console.log('private key (sign)', this.fingerprint);
 | 
			
		||||
					this._registerLoad(n);
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				this.pubk[x] = k.keys;
 | 
			
		||||
				k.keys.forEach((pubk) => {
 | 
			
		||||
					console.log('public key (' + x + ')', pubk.getFingerprint());
 | 
			
		||||
				});
 | 
			
		||||
				this._registerLoad(n);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private _registerLoad(b:number) {
 | 
			
		||||
		this.loads |= b;
 | 
			
		||||
		if (this.loads == this.loadsTarget) {
 | 
			
		||||
			this.onload(this);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getTrustedKeys(): Array<any> {
 | 
			
		||||
		return this.pubk['trusted'];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getTrustedActiveKeys(): Array<any> {
 | 
			
		||||
		return this.pubk['active'];
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getEncryptKeys(): Array<any> {
 | 
			
		||||
		return this.pubk['encrypt'];
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getPrivateKey(): any {
 | 
			
		||||
		return this.pk;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getFingerprint(): string {
 | 
			
		||||
		return this.fingerprint;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PGPSigner implements Signer {
 | 
			
		||||
 | 
			
		||||
	engine	= 'pgp'
 | 
			
		||||
	algo	= 'sha256'
 | 
			
		||||
	dgst:		string
 | 
			
		||||
	signature:	Signature
 | 
			
		||||
	keyStore:	KeyStore
 | 
			
		||||
	onsign:		(Signature) => void
 | 
			
		||||
	onverify:	(boolean) => void
 | 
			
		||||
 | 
			
		||||
	constructor(keyStore:KeyStore) {
 | 
			
		||||
		this.keyStore = keyStore
 | 
			
		||||
		this.onsign = (string) => {};
 | 
			
		||||
		this.onverify = (boolean) => {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public fingerprint(): string {
 | 
			
		||||
		return this.keyStore.getFingerprint();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public prepare(material:Signable):boolean {
 | 
			
		||||
		this.dgst = material.digest();
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public verify(digest:string, signature:Signature) {
 | 
			
		||||
		pgp.signature.readArmored(signature.data).then((s) => {
 | 
			
		||||
			const opts = {
 | 
			
		||||
				message: pgp.cleartext.fromText(digest),
 | 
			
		||||
				publicKeys: this.keyStore.getTrustedKeys(),
 | 
			
		||||
				signature: s,
 | 
			
		||||
			};
 | 
			
		||||
			pgp.verify(opts).then((v) => {
 | 
			
		||||
				let i = 0;
 | 
			
		||||
				for (i = 0; i < v.signatures.length; i++) {
 | 
			
		||||
					const s = v.signatures[i];
 | 
			
		||||
					if (s.valid) {
 | 
			
		||||
						this.onverify(s);
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				console.error('checked ' + i + ' signature(s) but none valid');
 | 
			
		||||
				this.onverify(false);
 | 
			
		||||
			});
 | 
			
		||||
		}).catch((e) => {
 | 
			
		||||
			console.error(e);
 | 
			
		||||
			this.onverify(false);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public sign(digest:string) {
 | 
			
		||||
		const m = pgp.cleartext.fromText(digest);
 | 
			
		||||
		const pk = this.keyStore.getPrivateKey();
 | 
			
		||||
		const opts = {
 | 
			
		||||
			message: m,
 | 
			
		||||
			privateKeys: [pk],
 | 
			
		||||
			detached: true,
 | 
			
		||||
		}
 | 
			
		||||
		pgp.sign(opts).then((s) => {
 | 
			
		||||
			this.signature = {
 | 
			
		||||
				engine: this.engine,
 | 
			
		||||
				algo: this.algo,
 | 
			
		||||
				data: s.signature,
 | 
			
		||||
				// TODO: fix for browser later
 | 
			
		||||
				digest: digest,
 | 
			
		||||
			};
 | 
			
		||||
			this.onsign(this.signature);
 | 
			
		||||
		}).catch((e) => {
 | 
			
		||||
			console.error(e);
 | 
			
		||||
			this.onsign(undefined);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	Signature,
 | 
			
		||||
	Authoritative,
 | 
			
		||||
       	Signer,
 | 
			
		||||
	KeyGetter,
 | 
			
		||||
	Signable,
 | 
			
		||||
	KeyStore,
 | 
			
		||||
	PGPSigner,
 | 
			
		||||
	PGPKeyStore,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										71
									
								
								apps/cic-meta/src/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								apps/cic-meta/src/config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as ini from 'ini';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
 | 
			
		||||
class Config {
 | 
			
		||||
 | 
			
		||||
	filepath: 	string
 | 
			
		||||
	store:		Object
 | 
			
		||||
	censor:		Array<string>
 | 
			
		||||
	require:	Array<string>
 | 
			
		||||
	env_prefix:	string
 | 
			
		||||
 | 
			
		||||
	constructor(filepath:string, env_prefix?:string) {
 | 
			
		||||
		this.filepath = filepath;
 | 
			
		||||
		this.store = {};
 | 
			
		||||
		this.censor = [];
 | 
			
		||||
		this.require = [];
 | 
			
		||||
		this.env_prefix = '';
 | 
			
		||||
		if (env_prefix !== undefined) {
 | 
			
		||||
			this.env_prefix = env_prefix + "_";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public process() {
 | 
			
		||||
		const d = fs.readdirSync(this.filepath);
 | 
			
		||||
		
 | 
			
		||||
		const r = /.*\.ini$/;
 | 
			
		||||
		for (let i = 0; i < d.length; i++) {
 | 
			
		||||
			const f = d[i];
 | 
			
		||||
			if (!f.match(r)) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const fp = path.join(this.filepath, f);
 | 
			
		||||
			const v = fs.readFileSync(fp, 'utf-8');
 | 
			
		||||
			const inid = ini.decode(v);
 | 
			
		||||
			const inik = Object.keys(inid);
 | 
			
		||||
			for (let j = 0; j < inik.length; j++) {
 | 
			
		||||
				const k_section = inik[j]
 | 
			
		||||
				const k = k_section.toUpperCase();
 | 
			
		||||
				Object.keys(inid[k_section]).forEach((k_directive) => {
 | 
			
		||||
					const kk = k_directive.toUpperCase();
 | 
			
		||||
					const kkk = k + '_' + kk;
 | 
			
		||||
 | 
			
		||||
					let r = inid[k_section][k_directive];
 | 
			
		||||
					const k_env = this.env_prefix + kkk
 | 
			
		||||
					const env = process.env[k_env];
 | 
			
		||||
					if (env !== undefined) {
 | 
			
		||||
						console.debug('Environment variable ' + k_env + ' overrides ' + kkk);
 | 
			
		||||
						r = env;
 | 
			
		||||
					}
 | 
			
		||||
					this.store[kkk] = r;
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public get(s:string) {
 | 
			
		||||
		return this.store[s];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public toString() {
 | 
			
		||||
		let s = '';
 | 
			
		||||
		Object.keys(this.store).forEach((k) => {
 | 
			
		||||
			s += k + '=' + this.store[k] + '\n';
 | 
			
		||||
		});
 | 
			
		||||
		return s;
 | 
			
		||||
	}	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Config };
 | 
			
		||||
							
								
								
									
										38
									
								
								apps/cic-meta/src/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								apps/cic-meta/src/constants.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
import { JSONSerializable } from './format';
 | 
			
		||||
 | 
			
		||||
const ENGINE_NAME = 'automerge';
 | 
			
		||||
const ENGINE_VERSION = '0.14.1';
 | 
			
		||||
 | 
			
		||||
const NETWORK_NAME = 'cic';
 | 
			
		||||
const NETWORK_VERSION = '1';
 | 
			
		||||
 | 
			
		||||
const CRYPTO_NAME = 'pgp';
 | 
			
		||||
const CRYPTO_VERSION = '2';
 | 
			
		||||
 | 
			
		||||
type VersionedSpec = {
 | 
			
		||||
	name:		string
 | 
			
		||||
	version: 	string
 | 
			
		||||
	ext?:		Object
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const engineSpec:VersionedSpec = {
 | 
			
		||||
	name: ENGINE_NAME,
 | 
			
		||||
	version: ENGINE_VERSION,			
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const cryptoSpec:VersionedSpec = {
 | 
			
		||||
	name: CRYPTO_NAME,
 | 
			
		||||
	version: CRYPTO_VERSION,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const networkSpec:VersionedSpec = {
 | 
			
		||||
	name: NETWORK_NAME,
 | 
			
		||||
	version: NETWORK_VERSION,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	engineSpec,
 | 
			
		||||
	cryptoSpec,
 | 
			
		||||
	networkSpec,
 | 
			
		||||
	VersionedSpec,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										27
									
								
								apps/cic-meta/src/crypto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								apps/cic-meta/src/crypto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import * as crypto from 'crypto';
 | 
			
		||||
 | 
			
		||||
const _algs = {
 | 
			
		||||
	'SHA-256': 'sha256',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cryptoWrapper() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
cryptoWrapper.prototype.digest = async function(s, d) {
 | 
			
		||||
	const h = crypto.createHash(_algs[s]);
 | 
			
		||||
	h.update(d);
 | 
			
		||||
	return h.digest();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let subtle = undefined;
 | 
			
		||||
if (typeof window !== 'undefined') {
 | 
			
		||||
	subtle = window.crypto.subtle;
 | 
			
		||||
} else {
 | 
			
		||||
	subtle = new cryptoWrapper();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	subtle,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										90
									
								
								apps/cic-meta/src/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								apps/cic-meta/src/db.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
import * as pg from 'pg';
 | 
			
		||||
import * as sqlite from 'sqlite3';
 | 
			
		||||
 | 
			
		||||
type DbConfig = {
 | 
			
		||||
	name:		string
 | 
			
		||||
	host:		string
 | 
			
		||||
	port:		number
 | 
			
		||||
	user:		string
 | 
			
		||||
	password:	string	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DbAdapter {
 | 
			
		||||
	query:	(s:string, callback:(e:any, rs:any) => void) => void
 | 
			
		||||
	close: 	() => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const re_creatematch = /^(CREATE)/i
 | 
			
		||||
const re_getmatch = /^(SELECT)/i;
 | 
			
		||||
const re_setmatch = /^(INSERT|UPDATE)/i;
 | 
			
		||||
 | 
			
		||||
class SqliteAdapter implements DbAdapter {
 | 
			
		||||
 | 
			
		||||
	db:		any
 | 
			
		||||
 | 
			
		||||
	constructor(dbConfig:DbConfig, callback?:(any) => void) {
 | 
			
		||||
		this.db = new sqlite.Database(dbConfig.name); //, callback);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public query(s:string, callback:(e:any, rs?:any) => void): void {
 | 
			
		||||
		const local_callback = (e, rs) => {
 | 
			
		||||
			let r = undefined;
 | 
			
		||||
			if (rs !== undefined) {
 | 
			
		||||
				r = {
 | 
			
		||||
					rowCount: rs.length,
 | 
			
		||||
					rows: rs,
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			callback(e, r);
 | 
			
		||||
		};
 | 
			
		||||
		if (s.match(re_getmatch)) {
 | 
			
		||||
			this.db.all(s, local_callback);
 | 
			
		||||
		} else if (s.match(re_setmatch)) {
 | 
			
		||||
			this.db.run(s, local_callback);
 | 
			
		||||
		} else if (s.match(re_creatematch)) {
 | 
			
		||||
			this.db.run(s, callback);
 | 
			
		||||
		} else {
 | 
			
		||||
			throw 'unhandled query';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public close() {
 | 
			
		||||
		this.db.close();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PostgresAdapter implements DbAdapter {
 | 
			
		||||
 | 
			
		||||
	db:		any
 | 
			
		||||
 | 
			
		||||
	constructor(dbConfig:DbConfig) {
 | 
			
		||||
		let o = dbConfig;
 | 
			
		||||
		o['database'] = o.name;
 | 
			
		||||
		this.db = new pg.Pool(o);
 | 
			
		||||
		return this.db;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public query(s:string, callback:(e:any, rs:any) => void): void {
 | 
			
		||||
		this.db.query(s, (e, rs) => {
 | 
			
		||||
			let r = {
 | 
			
		||||
				length: rs.rowCount,
 | 
			
		||||
			}
 | 
			
		||||
			rs.length = rs.rowCount;
 | 
			
		||||
			if (e === undefined) {
 | 
			
		||||
				e = null;
 | 
			
		||||
			}
 | 
			
		||||
			console.debug(e, rs);
 | 
			
		||||
			callback(e, rs);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public close() {
 | 
			
		||||
		this.db.end();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	DbConfig,
 | 
			
		||||
	SqliteAdapter,
 | 
			
		||||
	PostgresAdapter,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								apps/cic-meta/src/digest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								apps/cic-meta/src/digest.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
import * as crypto from './crypto';
 | 
			
		||||
 | 
			
		||||
interface Addressable {
 | 
			
		||||
	key(): string
 | 
			
		||||
	digest(): string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stringToBytes(s:string) {
 | 
			
		||||
	const a = new Uint8Array(20);
 | 
			
		||||
	let j = 2;
 | 
			
		||||
	for (let i = 0; i < a.byteLength; i++) {
 | 
			
		||||
		const n = parseInt(s.substring(j, j+2), 16);
 | 
			
		||||
		a[i] = n;
 | 
			
		||||
		j += 2;
 | 
			
		||||
	}
 | 
			
		||||
	return a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function bytesToHex(a:Uint8Array) {
 | 
			
		||||
	let s = '';
 | 
			
		||||
	for (let i = 0; i < a.byteLength; i++) {
 | 
			
		||||
		const h = '00' + a[i].toString(16);
 | 
			
		||||
		s += h.slice(-2);
 | 
			
		||||
	}
 | 
			
		||||
	return s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function mergeKey(a:Uint8Array, s:Uint8Array) {
 | 
			
		||||
	const y = new Uint8Array(a.byteLength + s.byteLength);
 | 
			
		||||
	for (let i = 0; i < a.byteLength; i++) {
 | 
			
		||||
		y[i] = a[i];
 | 
			
		||||
	}
 | 
			
		||||
	for (let i = 0; i < s.byteLength; i++) {
 | 
			
		||||
		y[a.byteLength + i] = s[i];
 | 
			
		||||
	}
 | 
			
		||||
	const z = await crypto.subtle.digest('SHA-256', y);
 | 
			
		||||
	return bytesToHex(new Uint8Array(z));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function toKey(v:string, salt:string) {
 | 
			
		||||
	const a = stringToBytes(v);
 | 
			
		||||
	const s = new TextEncoder().encode(salt);
 | 
			
		||||
	return await mergeKey(a, s);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function toAddressKey(zeroExHex:string, salt:string) {
 | 
			
		||||
	const a = addressToBytes(zeroExHex);
 | 
			
		||||
	const s = new TextEncoder().encode(salt);
 | 
			
		||||
	return await mergeKey(a, s);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const re_addrHex = /^0[xX][a-fA-F0-9]{40}$/;
 | 
			
		||||
function addressToBytes(s:string) {
 | 
			
		||||
	if (!s.match(re_addrHex)) {
 | 
			
		||||
		throw 'invalid address hex';
 | 
			
		||||
	}
 | 
			
		||||
	return stringToBytes(s);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	toKey,
 | 
			
		||||
	toAddressKey,
 | 
			
		||||
	bytesToHex,
 | 
			
		||||
	addressToBytes,
 | 
			
		||||
	Addressable,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								apps/cic-meta/src/dispatch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								apps/cic-meta/src/dispatch.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
import { Syncable } from './sync';
 | 
			
		||||
import { Store } from './store';
 | 
			
		||||
import { PubSub } from './transport';
 | 
			
		||||
 | 
			
		||||
function toIndexKey(id:string):string {
 | 
			
		||||
	const d = Date.now();
 | 
			
		||||
	return d + '_' + id + '_' + uuidv4();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const _re_indexKey = /^\d+_(.+)_[-\d\w]+$/;
 | 
			
		||||
function fromIndexKey(s:string):string {
 | 
			
		||||
	const m = s.match(_re_indexKey);
 | 
			
		||||
	if (m === null) {
 | 
			
		||||
		throw 'Invalid index key';
 | 
			
		||||
	}
 | 
			
		||||
	return m[1];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Dispatcher {
 | 
			
		||||
 | 
			
		||||
	idx:		Array<string>
 | 
			
		||||
	syncer:		PubSub
 | 
			
		||||
	store:		Store
 | 
			
		||||
 | 
			
		||||
	constructor(store:Store, syncer:PubSub) {
 | 
			
		||||
		this.idx = new Array<string>()
 | 
			
		||||
		this.syncer = syncer;
 | 
			
		||||
		this.store = store;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public isDirty(): boolean {
 | 
			
		||||
		return this.idx.length > 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public add(id:string, item:Syncable): string {
 | 
			
		||||
		const v = item.toJSON();
 | 
			
		||||
		const k = toIndexKey(id);
 | 
			
		||||
		this.store.put(k, v, true);
 | 
			
		||||
		localStorage.setItem(k, v);
 | 
			
		||||
		this.idx.push(k);
 | 
			
		||||
		return k;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public sync(offset:number): number {
 | 
			
		||||
		let i = 0;
 | 
			
		||||
		this.idx.forEach((k) => {
 | 
			
		||||
			const v = localStorage.getItem(k);
 | 
			
		||||
			const k_id = fromIndexKey(k);
 | 
			
		||||
			this.syncer.pub(v); // this must block until guaranteed delivery
 | 
			
		||||
			localStorage.removeItem(k);
 | 
			
		||||
			i++;
 | 
			
		||||
		});
 | 
			
		||||
		return i;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Dispatcher, toIndexKey, fromIndexKey }
 | 
			
		||||
							
								
								
									
										5
									
								
								apps/cic-meta/src/format.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/cic-meta/src/format.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
interface JSONSerializable {
 | 
			
		||||
	toJSON(): string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { JSONSerializable };
 | 
			
		||||
							
								
								
									
										4
									
								
								apps/cic-meta/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/cic-meta/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
export { PGPSigner, PGPKeyStore } from './auth';
 | 
			
		||||
export { Envelope, Syncable } from './sync';
 | 
			
		||||
export { User } from './assets/user';
 | 
			
		||||
export { Phone } from './assets/phone';
 | 
			
		||||
							
								
								
									
										9
									
								
								apps/cic-meta/src/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/cic-meta/src/store.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
import { Syncable } from './sync';
 | 
			
		||||
 | 
			
		||||
interface Store {
 | 
			
		||||
	put(string, Syncable, boolean?)
 | 
			
		||||
	get(string):Syncable
 | 
			
		||||
	delete(string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Store };
 | 
			
		||||
							
								
								
									
										266
									
								
								apps/cic-meta/src/sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								apps/cic-meta/src/sync.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,266 @@
 | 
			
		||||
import * as Automerge from 'automerge';
 | 
			
		||||
 | 
			
		||||
import { JSONSerializable } from './format';
 | 
			
		||||
 | 
			
		||||
import { Authoritative, Signer, PGPSigner, Signable, Signature } from './auth';
 | 
			
		||||
 | 
			
		||||
import { engineSpec, cryptoSpec, networkSpec, VersionedSpec  } from './constants';
 | 
			
		||||
 | 
			
		||||
const fullSpec:VersionedSpec = {
 | 
			
		||||
	name: 'cic',
 | 
			
		||||
	version: '1',
 | 
			
		||||
	ext: {
 | 
			
		||||
		network: cryptoSpec,
 | 
			
		||||
		engine: engineSpec,
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Envelope {
 | 
			
		||||
 | 
			
		||||
	o = fullSpec
 | 
			
		||||
 | 
			
		||||
	constructor(payload:Object) {
 | 
			
		||||
		this.set(payload);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public set(payload:Object) {
 | 
			
		||||
		this.o['payload'] = payload
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public get():string {
 | 
			
		||||
		return this.o['payload'];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public toJSON() {
 | 
			
		||||
		return JSON.stringify(this.o);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static fromJSON(s:string): Envelope {
 | 
			
		||||
		const e = new Envelope(undefined);
 | 
			
		||||
		e.o = JSON.parse(s);
 | 
			
		||||
		return e;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public unwrap(): Syncable {
 | 
			
		||||
		return Syncable.fromJSON(this.o['payload']);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ArgPair {
 | 
			
		||||
 | 
			
		||||
	k:string
 | 
			
		||||
	v:any
 | 
			
		||||
 | 
			
		||||
	constructor(k:string, v:any) {
 | 
			
		||||
		this.k = k;
 | 
			
		||||
		this.v = v;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SignablePart implements Signable {
 | 
			
		||||
 | 
			
		||||
	s: string
 | 
			
		||||
 | 
			
		||||
	constructor(s:string) {
 | 
			
		||||
		this.s = s;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public digest():string {
 | 
			
		||||
		return this.s;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function orderDict(src) {
 | 
			
		||||
	let dst;
 | 
			
		||||
	if (Array.isArray(src)) {
 | 
			
		||||
		dst = [];
 | 
			
		||||
		src.forEach((v) => {
 | 
			
		||||
			if (typeof(v) == 'object') {
 | 
			
		||||
				v = orderDict(v);
 | 
			
		||||
			}
 | 
			
		||||
			dst.push(v);
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		dst = {}
 | 
			
		||||
		Object.keys(src).sort().forEach((k) => {
 | 
			
		||||
			let v = src[k];
 | 
			
		||||
			if (typeof(v) == 'object') {
 | 
			
		||||
				v = orderDict(v);
 | 
			
		||||
			}
 | 
			
		||||
			dst[k] = v;
 | 
			
		||||
		});
 | 
			
		||||
	} 
 | 
			
		||||
	return dst;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Syncable implements JSONSerializable, Authoritative {
 | 
			
		||||
 | 
			
		||||
	id:		string
 | 
			
		||||
	timestamp:	number
 | 
			
		||||
	m:		any // automerge object
 | 
			
		||||
	e:		Envelope
 | 
			
		||||
	signer:		Signer
 | 
			
		||||
	onwrap: 	(string) => void
 | 
			
		||||
	onauthenticate:	(boolean) => void
 | 
			
		||||
 | 
			
		||||
	// TODO: Move data to sub-object so timestamp, id, signature don't collide
 | 
			
		||||
	constructor(id:string, v:Object) {
 | 
			
		||||
		this.id = id;
 | 
			
		||||
		const o = {
 | 
			
		||||
			'id': id,
 | 
			
		||||
			'timestamp': Math.floor(Date.now() / 1000),
 | 
			
		||||
			'data': v,
 | 
			
		||||
		}
 | 
			
		||||
		//this.m = Automerge.from(v)
 | 
			
		||||
		this.m = Automerge.from(o)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public setSigner(signer:Signer) {
 | 
			
		||||
		this.signer = signer;
 | 
			
		||||
		this.signer.onsign = (s) => {
 | 
			
		||||
			this.wrap(s);
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: To keep integrity, the non-link key/value pairs for each step also need to be hashed
 | 
			
		||||
	public digest(): string {
 | 
			
		||||
		const links = [];
 | 
			
		||||
		Automerge.getAllChanges(this.m).forEach((ch:Object) => {
 | 
			
		||||
			const op:Array<any> = ch['ops'];
 | 
			
		||||
			ch['ops'].forEach((op:Array<Object>) => {
 | 
			
		||||
				if (op['action'] == 'link') {
 | 
			
		||||
					//console.log('op link', op);
 | 
			
		||||
					links.push([op['obj'], op['value']]);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
		//return JSON.stringify(links);
 | 
			
		||||
		const j = JSON.stringify(links);
 | 
			
		||||
		return Buffer.from(j).toString('base64');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private wrap(s:any) {
 | 
			
		||||
		this.m = Automerge.change(this.m, 'sign', (doc) => {
 | 
			
		||||
			doc['signature'] = s;
 | 
			
		||||
		});
 | 
			
		||||
		this.e = new Envelope(this.toJSON());
 | 
			
		||||
		console.log('wrappin s', s, typeof(s));
 | 
			
		||||
		this.e.o['digest'] = s.digest;
 | 
			
		||||
		if (this.onwrap !== undefined) {
 | 
			
		||||
			this.onwrap(this.e);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
//	private _verifyLoop(i:number, history:Array<any>, signable:Signable, result:boolean) {
 | 
			
		||||
//		if (!result) {
 | 
			
		||||
//			this.onauthenticate(false);
 | 
			
		||||
//			return;
 | 
			
		||||
//		} else if (history.length == 0) {
 | 
			
		||||
//			this.onauthenticate(true);
 | 
			
		||||
//			return;
 | 
			
		||||
//		}
 | 
			
		||||
//		const h = history.shift()
 | 
			
		||||
//		if (i % 2 == 0) {
 | 
			
		||||
//			i++;
 | 
			
		||||
//			signable = {
 | 
			
		||||
//				digest: () => {
 | 
			
		||||
//					return Automerge.save(h.snapshot)
 | 
			
		||||
//				},
 | 
			
		||||
//			};
 | 
			
		||||
//			this._verifyLoop(i, history, signable, true);
 | 
			
		||||
//		} else {
 | 
			
		||||
//			i++;
 | 
			
		||||
//			const signature = h.snapshot['signature'];
 | 
			
		||||
//			console.debug('signature', signature, signable.digest());
 | 
			
		||||
//			this.signer.onverify = (v) => {
 | 
			
		||||
//				this._verifyLoop(i, history, signable, v)
 | 
			
		||||
//			}
 | 
			
		||||
//			this.signer.verify(signable, signature);
 | 
			
		||||
//		}
 | 
			
		||||
//	}
 | 
			
		||||
//
 | 
			
		||||
//	// TODO: This should replay the graph and check signatures on each step
 | 
			
		||||
//	public _authenticate(full:boolean=false) {
 | 
			
		||||
//		let h = Automerge.getHistory(this.m);
 | 
			
		||||
//		h.forEach((m) => {
 | 
			
		||||
//			//console.debug(m.snapshot);
 | 
			
		||||
//		});
 | 
			
		||||
//		const signable = {
 | 
			
		||||
//			digest: () => { return '' },
 | 
			
		||||
//		}
 | 
			
		||||
//		if (!full) {
 | 
			
		||||
//			h = h.slice(h.length-2);
 | 
			
		||||
//		}
 | 
			
		||||
//		this._verifyLoop(0, h, signable, true);
 | 
			
		||||
//	}
 | 
			
		||||
 | 
			
		||||
	public authenticate(full:boolean=false) {
 | 
			
		||||
		if (full) {
 | 
			
		||||
			console.warn('only doing shallow authentication for now, sorry');			
 | 
			
		||||
		}
 | 
			
		||||
		//console.log('authenticating', signable.digest());
 | 
			
		||||
		//console.log('signature', this.m.signature);
 | 
			
		||||
		this.signer.onverify = (v) => {
 | 
			
		||||
			//this._verifyLoop(i, history, signable, v)
 | 
			
		||||
			this.onauthenticate(v);
 | 
			
		||||
		}
 | 
			
		||||
		this.signer.verify(this.m.signature.digest, this.m.signature);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public sign() {
 | 
			
		||||
		//this.signer.prepare(this);
 | 
			
		||||
		this.signer.sign(this.digest());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public update(changes:Array<ArgPair>, changesDescription:string) {
 | 
			
		||||
		this.m = Automerge.change(this.m, changesDescription, (m) => {
 | 
			
		||||
			changes.forEach((c) => {
 | 
			
		||||
				let path = c.k.split('.');
 | 
			
		||||
				let target = m['data'];
 | 
			
		||||
				while (path.length > 1) {
 | 
			
		||||
					const part = path.shift();
 | 
			
		||||
					target = target[part];
 | 
			
		||||
				}
 | 
			
		||||
				target[path[0]] = c.v;
 | 
			
		||||
			});
 | 
			
		||||
			m['timestamp'] = Math.floor(Date.now() / 1000);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public replace(o:Object, changesDescription:string) {
 | 
			
		||||
		this.m = Automerge.change(this.m, changesDescription, (m) => {
 | 
			
		||||
			Object.keys(o).forEach((k) => {
 | 
			
		||||
				m['data'][k] = o[k];
 | 
			
		||||
			});
 | 
			
		||||
			Object.keys(m).forEach((k) => {
 | 
			
		||||
				if (o[k] == undefined) {
 | 
			
		||||
					delete m['data'][k];
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			m['timestamp'] = Math.floor(Date.now() / 1000);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public merge(s:Syncable) {
 | 
			
		||||
		this.m = Automerge.merge(s.m, this.m);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public toJSON(): string {
 | 
			
		||||
		const s = Automerge.save(this.m);
 | 
			
		||||
		const o = JSON.parse(s);
 | 
			
		||||
		const oo = orderDict(o)
 | 
			
		||||
		return JSON.stringify(oo);
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static fromJSON(s:string): Syncable {
 | 
			
		||||
		const doc = Automerge.load(s);
 | 
			
		||||
		let y = new Syncable(doc['id'], {});
 | 
			
		||||
		y.m = doc
 | 
			
		||||
		return y
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { JSONSerializable, Syncable, ArgPair, Envelope };
 | 
			
		||||
							
								
								
									
										11
									
								
								apps/cic-meta/src/transport.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/cic-meta/src/transport.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
interface SubConsumer {
 | 
			
		||||
	post(string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface PubSub {
 | 
			
		||||
	pub(v:string):boolean
 | 
			
		||||
	close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { PubSub, SubConsumer };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								apps/cic-meta/tests/1_basic.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								apps/cic-meta/tests/1_basic.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
import * as Automerge from 'automerge';
 | 
			
		||||
import assert = require('assert');
 | 
			
		||||
 | 
			
		||||
import { Dispatcher, toIndexKey, fromIndexKey } from '../src/dispatch';
 | 
			
		||||
import { User } from '../src/assets/user';
 | 
			
		||||
import { Syncable, ArgPair } from '../src/sync';
 | 
			
		||||
 | 
			
		||||
import { MockSigner, MockStore } from './mock';
 | 
			
		||||
 | 
			
		||||
describe('basic', () => {
 | 
			
		||||
 | 
			
		||||
	it('store', () => {
 | 
			
		||||
		const store = new MockStore('s');
 | 
			
		||||
		assert.equal(store.name, 's');
 | 
			
		||||
 | 
			
		||||
		const mockSigner = new MockSigner();
 | 
			
		||||
		const v = new Syncable('foo', {baz: 42});
 | 
			
		||||
		v.setSigner(mockSigner);
 | 
			
		||||
		store.put('foo', v);
 | 
			
		||||
		const one = store.get('foo').toJSON();
 | 
			
		||||
		const vv = new Syncable('bar', {baz: 666});
 | 
			
		||||
		vv.setSigner(mockSigner);
 | 
			
		||||
		assert.throws(() => {
 | 
			
		||||
			store.put('foo', vv)
 | 
			
		||||
		});
 | 
			
		||||
		store.put('foo', vv, true);
 | 
			
		||||
		const other = store.get('foo').toJSON();
 | 
			
		||||
		assert.notEqual(one, other);
 | 
			
		||||
		store.delete('foo');
 | 
			
		||||
		assert.equal(store.get('foo'), undefined);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('add_doc_to_dispatcher', () => {
 | 
			
		||||
		const store = new MockStore('s');
 | 
			
		||||
		//const syncer = new MockSyncer();
 | 
			
		||||
		const dispatcher = new Dispatcher(store, undefined);
 | 
			
		||||
		const user = new User('foo'); 
 | 
			
		||||
		dispatcher.add(user.id, user);
 | 
			
		||||
		assert(dispatcher.isDirty());
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('dispatch_keyindex', () => {
 | 
			
		||||
		const s = 'foo';
 | 
			
		||||
		const k = toIndexKey(s);
 | 
			
		||||
		const v = fromIndexKey(k);
 | 
			
		||||
		assert.equal(s, v);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										212
									
								
								apps/cic-meta/tests/2_sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								apps/cic-meta/tests/2_sync.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,212 @@
 | 
			
		||||
import * as Automerge from 'automerge';
 | 
			
		||||
import assert = require('assert');
 | 
			
		||||
 | 
			
		||||
import * as pgp from 'openpgp';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
 | 
			
		||||
import { PGPSigner } from '../src/auth';
 | 
			
		||||
 | 
			
		||||
import { Syncable, ArgPair } from '../src/sync';
 | 
			
		||||
 | 
			
		||||
import { MockKeyStore, MockSigner } from './mock';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('sync', async () => {
 | 
			
		||||
	it('sync_merge', () => {
 | 
			
		||||
		const mockSigner = new MockSigner();
 | 
			
		||||
		const s = new Syncable('foo', {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		s.setSigner(mockSigner);
 | 
			
		||||
		const changePair = new ArgPair('xyzzy', 42);
 | 
			
		||||
		s.update([changePair], 'ch-ch-cha-changes');
 | 
			
		||||
		assert.equal(s.m.data['xyzzy'], 42)
 | 
			
		||||
		assert.equal(s.m.data['bar'], 'baz')
 | 
			
		||||
		assert.equal(s.m['id'], 'foo')
 | 
			
		||||
		assert.equal(Automerge.getHistory(s.m).length, 2);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('sync_serialize', () => {
 | 
			
		||||
		const mockSigner = new MockSigner();
 | 
			
		||||
		const s = new Syncable('foo', {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		s.setSigner(mockSigner);
 | 
			
		||||
		const j = s.toJSON();
 | 
			
		||||
		const ss = Syncable.fromJSON(j);
 | 
			
		||||
		assert.equal(ss.m['id'], 'foo');
 | 
			
		||||
		assert.equal(ss.m['data']['bar'], 'baz');
 | 
			
		||||
		assert.equal(Automerge.getHistory(ss.m).length, 1);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('sync_sign_and_wrap', () => {
 | 
			
		||||
		const mockSigner = new MockSigner();
 | 
			
		||||
		const s = new Syncable('foo', {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		s.setSigner(mockSigner);
 | 
			
		||||
		s.onwrap = (e) => {
 | 
			
		||||
			const j = e.toJSON();
 | 
			
		||||
			const v = JSON.parse(j);
 | 
			
		||||
			assert.deepEqual(v.payload, e.o.payload);
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		s.sign();
 | 
			
		||||
	});
 | 
			
		||||
	it('sync_verify_success', async () => {
 | 
			
		||||
		const pksa = fs.readFileSync(__dirname + '/privatekeys.asc');
 | 
			
		||||
		const pks = await pgp.key.readArmored(pksa);
 | 
			
		||||
		await pks.keys[0].decrypt('merman');
 | 
			
		||||
		await pks.keys[1].decrypt('beastman');
 | 
			
		||||
 | 
			
		||||
		const pubksa = fs.readFileSync(__dirname + '/publickeys.asc');
 | 
			
		||||
		const pubks = await pgp.key.readArmored(pubksa);
 | 
			
		||||
 | 
			
		||||
		const oneStore = new MockKeyStore(pks.keys[0], pubks.keys);
 | 
			
		||||
		const twoStore = new MockKeyStore(pks.keys[1], pubks.keys);
 | 
			
		||||
		const threeStore = new MockKeyStore(pks.keys[2], [pubks.keys[0], pubks.keys[2]]);
 | 
			
		||||
 | 
			
		||||
		const oneSigner = new PGPSigner(oneStore);
 | 
			
		||||
		const twoSigner = new PGPSigner(twoStore);
 | 
			
		||||
		const threeSigner = new PGPSigner(threeStore);
 | 
			
		||||
 | 
			
		||||
		const x = new Syncable('foo', {
 | 
			
		||||
			bar: 'baz',		  
 | 
			
		||||
		});
 | 
			
		||||
		x.setSigner(oneSigner);
 | 
			
		||||
 | 
			
		||||
		// TODO: make this look better
 | 
			
		||||
		x.onwrap = (e) => {
 | 
			
		||||
			let updateData = new ArgPair('bar', 'xyzzy');
 | 
			
		||||
			x.update([updateData], 'change one');
 | 
			
		||||
 | 
			
		||||
			x.onwrap = (e) => {
 | 
			
		||||
				x.setSigner(twoSigner);
 | 
			
		||||
				updateData = new ArgPair('bar', 42);
 | 
			
		||||
				x.update([updateData], 'change two');
 | 
			
		||||
 | 
			
		||||
				x.onwrap = (e) => {
 | 
			
		||||
					const p = e.unwrap();
 | 
			
		||||
					p.setSigner(twoSigner);
 | 
			
		||||
					p.onauthenticate = (v) => {
 | 
			
		||||
						assert(v);
 | 
			
		||||
					}
 | 
			
		||||
					p.authenticate();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				x.sign();
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			x.sign();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		x.sign();
 | 
			
		||||
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('sync_verify_fail', async () => {
 | 
			
		||||
		const pksa = fs.readFileSync(__dirname + '/privatekeys.asc');
 | 
			
		||||
		const pks = await pgp.key.readArmored(pksa);
 | 
			
		||||
		await pks.keys[0].decrypt('merman');
 | 
			
		||||
		await pks.keys[1].decrypt('beastman');
 | 
			
		||||
 | 
			
		||||
		const pubksa = fs.readFileSync(__dirname + '/publickeys.asc');
 | 
			
		||||
		const pubks = await pgp.key.readArmored(pubksa);
 | 
			
		||||
 | 
			
		||||
		const oneStore = new MockKeyStore(pks.keys[0], pubks.keys);
 | 
			
		||||
		const twoStore = new MockKeyStore(pks.keys[1], pubks.keys);
 | 
			
		||||
		const threeStore = new MockKeyStore(pks.keys[2], [pubks.keys[0], pubks.keys[2]]);
 | 
			
		||||
 | 
			
		||||
		const oneSigner = new PGPSigner(oneStore);
 | 
			
		||||
		const twoSigner = new PGPSigner(twoStore);
 | 
			
		||||
		const threeSigner = new PGPSigner(threeStore);
 | 
			
		||||
 | 
			
		||||
		const x = new Syncable('foo', {
 | 
			
		||||
			bar: 'baz',		  
 | 
			
		||||
		});
 | 
			
		||||
		x.setSigner(oneSigner);
 | 
			
		||||
 | 
			
		||||
		// TODO: make this look better
 | 
			
		||||
		x.onwrap = (e) => {
 | 
			
		||||
			let updateData = new ArgPair('bar', 'xyzzy');
 | 
			
		||||
			x.update([updateData], 'change one');
 | 
			
		||||
 | 
			
		||||
			x.onwrap = (e) => {
 | 
			
		||||
				x.setSigner(twoSigner);
 | 
			
		||||
				updateData = new ArgPair('bar', 42);
 | 
			
		||||
				x.update([updateData], 'change two');
 | 
			
		||||
 | 
			
		||||
				x.onwrap = (e) => {
 | 
			
		||||
					const p = e.unwrap();
 | 
			
		||||
					p.setSigner(threeSigner);
 | 
			
		||||
					p.onauthenticate = (v) => {
 | 
			
		||||
						assert(!v);
 | 
			
		||||
					}
 | 
			
		||||
					p.authenticate();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				x.sign();
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			x.sign();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		x.sign();
 | 
			
		||||
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	xit('sync_verify_shallow_tricked', async () => {
 | 
			
		||||
		const pksa = fs.readFileSync(__dirname + '/privatekeys.asc');
 | 
			
		||||
		const pks = await pgp.key.readArmored(pksa);
 | 
			
		||||
		await pks.keys[0].decrypt('merman');
 | 
			
		||||
		await pks.keys[1].decrypt('beastman');
 | 
			
		||||
 | 
			
		||||
		const pubksa = fs.readFileSync(__dirname + '/publickeys.asc');
 | 
			
		||||
		const pubks = await pgp.key.readArmored(pubksa);
 | 
			
		||||
 | 
			
		||||
		const oneStore = new MockKeyStore(pks.keys[0], pubks.keys);
 | 
			
		||||
		const twoStore = new MockKeyStore(pks.keys[1], pubks.keys);
 | 
			
		||||
		const threeStore = new MockKeyStore(pks.keys[2], [pubks.keys[0], pubks.keys[2]]);
 | 
			
		||||
 | 
			
		||||
		const oneSigner = new PGPSigner(oneStore);
 | 
			
		||||
		const twoSigner = new PGPSigner(twoStore);
 | 
			
		||||
		const threeSigner = new PGPSigner(threeStore);
 | 
			
		||||
 | 
			
		||||
		const x = new Syncable('foo', {
 | 
			
		||||
			bar: 'baz',		  
 | 
			
		||||
		});
 | 
			
		||||
		x.setSigner(twoSigner);
 | 
			
		||||
 | 
			
		||||
		// TODO: make this look better
 | 
			
		||||
		x.onwrap = (e) => {
 | 
			
		||||
			let updateData = new ArgPair('bar', 'xyzzy');
 | 
			
		||||
			x.update([updateData], 'change one');
 | 
			
		||||
 | 
			
		||||
			x.onwrap = (e) => {
 | 
			
		||||
				updateData = new ArgPair('bar', 42);
 | 
			
		||||
				x.update([updateData], 'change two');
 | 
			
		||||
				x.setSigner(oneSigner);
 | 
			
		||||
 | 
			
		||||
				x.onwrap = (e) => {
 | 
			
		||||
					const p = e.unwrap();
 | 
			
		||||
					p.setSigner(threeSigner);
 | 
			
		||||
					p.onauthenticate = (v) => {
 | 
			
		||||
						assert(v);
 | 
			
		||||
						p.onauthenticate = (v) => {
 | 
			
		||||
							assert(!v);
 | 
			
		||||
						}
 | 
			
		||||
						p.authenticate(true);
 | 
			
		||||
					}
 | 
			
		||||
					p.authenticate();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				x.sign();
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			x.sign();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		x.sign();
 | 
			
		||||
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										14
									
								
								apps/cic-meta/tests/3_transport.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/cic-meta/tests/3_transport.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
 | 
			
		||||
import { MockPubSub, MockConsumer } from './mock';
 | 
			
		||||
 | 
			
		||||
describe('transport', () => {
 | 
			
		||||
	it('pub_sub', () => {
 | 
			
		||||
		const c = new MockConsumer();
 | 
			
		||||
		const ps = new MockPubSub('foo', c);
 | 
			
		||||
		ps.pub('foo');	
 | 
			
		||||
		ps.pub('bar');
 | 
			
		||||
		ps.flush();
 | 
			
		||||
		assert.deepEqual(c.omnoms, ['foo', 'bar']);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										46
									
								
								apps/cic-meta/tests/4_auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								apps/cic-meta/tests/4_auth.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
import assert = require('assert');
 | 
			
		||||
import pgp = require('openpgp');
 | 
			
		||||
import crypto  = require('crypto');
 | 
			
		||||
 | 
			
		||||
import { Syncable, ArgPair } from '../src/sync';
 | 
			
		||||
 | 
			
		||||
import { MockKeyStore, MockSignable } from './mock';
 | 
			
		||||
 | 
			
		||||
import { PGPSigner } from '../src/auth';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('auth', async () => {
 | 
			
		||||
	await it('digest', async () => {
 | 
			
		||||
		const opts = {
 | 
			
		||||
			userIds: [
 | 
			
		||||
				{
 | 
			
		||||
					name: 'John Marston',
 | 
			
		||||
					email: 'red@dead.com',
 | 
			
		||||
				},
 | 
			
		||||
			],
 | 
			
		||||
			numBits: 2048,
 | 
			
		||||
			passphrase: 'foo',
 | 
			
		||||
		};
 | 
			
		||||
		const pkgen = await pgp.generateKey(opts);
 | 
			
		||||
		const pka = pkgen.privateKeyArmored;
 | 
			
		||||
		const pks = await pgp.key.readArmored(pka);
 | 
			
		||||
		await pks.keys[0].decrypt('foo');
 | 
			
		||||
		const pubka = pkgen.publicKeyArmored;
 | 
			
		||||
		const pubks = await pgp.key.readArmored(pubka);
 | 
			
		||||
		const keyStore = new MockKeyStore(pks.keys[0], pubks.keys);
 | 
			
		||||
		const s = new PGPSigner(keyStore);
 | 
			
		||||
 | 
			
		||||
		const message = await pgp.cleartext.fromText('foo');
 | 
			
		||||
		s.onverify = (ok) => {
 | 
			
		||||
			assert(ok);
 | 
			
		||||
		}
 | 
			
		||||
		s.onsign = (signature) => {
 | 
			
		||||
			s.onverify((v) => {
 | 
			
		||||
				console.log('bar', v);
 | 
			
		||||
			});
 | 
			
		||||
			s.verify('foo', signature);
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		await s.sign('foo');
 | 
			
		||||
	});
 | 
			
		||||
});	
 | 
			
		||||
							
								
								
									
										47
									
								
								apps/cic-meta/tests/999_functional.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								apps/cic-meta/tests/999_functional.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import * as pgp from 'openpgp';
 | 
			
		||||
 | 
			
		||||
import { Dispatcher } from '../src/dispatch';
 | 
			
		||||
import { User } from '../src/assets/user';
 | 
			
		||||
import { PGPSigner, KeyStore } from '../src/auth';
 | 
			
		||||
import { SubConsumer } from '../src/transport';
 | 
			
		||||
 | 
			
		||||
import { MockStore, MockPubSub, MockConsumer, MockKeyStore } from './mock';
 | 
			
		||||
 | 
			
		||||
async function createKeyStore() {
 | 
			
		||||
	const opts = {
 | 
			
		||||
		userIds: [
 | 
			
		||||
			{
 | 
			
		||||
				name: 'John Marston',
 | 
			
		||||
				email: 'red@dead.com',
 | 
			
		||||
			},
 | 
			
		||||
		],
 | 
			
		||||
		numBits: 2048,
 | 
			
		||||
		passphrase: 'foo',
 | 
			
		||||
	};
 | 
			
		||||
	const pkgen = await pgp.generateKey(opts);
 | 
			
		||||
	const pka = pkgen.privateKeyArmored;
 | 
			
		||||
	const pks = await pgp.key.readArmored(pka);
 | 
			
		||||
	await pks.keys[0].decrypt('foo');
 | 
			
		||||
	return new MockKeyStore(pks.keys[0], []);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('fullchain', async () => {
 | 
			
		||||
	it('dispatch_and_publish_user', async () => {
 | 
			
		||||
		const g = await createKeyStore();
 | 
			
		||||
		const n = new PGPSigner(g);
 | 
			
		||||
		const u = new User('u1', {});
 | 
			
		||||
		u.setSigner(n);
 | 
			
		||||
		u.setName('Nico', 'Bellic');
 | 
			
		||||
		const s = new MockStore('fooStore');
 | 
			
		||||
		const c = new MockConsumer();
 | 
			
		||||
		const p = new MockPubSub('fooPubSub', c);
 | 
			
		||||
		const d = new Dispatcher(s, p);
 | 
			
		||||
		u.onwrap = (e) => {
 | 
			
		||||
			d.add(u.id, e);
 | 
			
		||||
			d.sync(0);
 | 
			
		||||
			assert.equal(p.pubs.length, 1);
 | 
			
		||||
		};
 | 
			
		||||
		u.sign();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										150
									
								
								apps/cic-meta/tests/mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								apps/cic-meta/tests/mock.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,150 @@
 | 
			
		||||
import * as crypto from 'crypto';
 | 
			
		||||
 | 
			
		||||
import { Signable, Signature, KeyStore } from '../src/auth';
 | 
			
		||||
import { Store } from '../src/store';
 | 
			
		||||
import { PubSub, SubConsumer } from '../src/transport';
 | 
			
		||||
import { Syncable } from '../src/sync';
 | 
			
		||||
 | 
			
		||||
class MockStore implements Store {
 | 
			
		||||
	
 | 
			
		||||
	contents:	Object
 | 
			
		||||
	name:		string
 | 
			
		||||
 | 
			
		||||
	constructor(name:string) {
 | 
			
		||||
		this.name = name;
 | 
			
		||||
		this.contents = {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public put(k:string, v:Syncable, existsOk = false) {
 | 
			
		||||
		if (!existsOk && this.contents[k] !== undefined) {
 | 
			
		||||
			throw '"' + k + '" already exists in store ' + this.name;
 | 
			
		||||
		} 
 | 
			
		||||
		this.contents[k] = v;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public get(k:string): Syncable {
 | 
			
		||||
		return this.contents[k];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public delete(k:string) {
 | 
			
		||||
		delete this.contents[k];
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MockSigner {
 | 
			
		||||
	onsign: (string) => void
 | 
			
		||||
	onverify: (boolean) => void
 | 
			
		||||
	public verify(src:string, signature:Signature) {
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public sign(s:string):boolean {
 | 
			
		||||
		this.onsign('there would be a signature here');
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public prepare(m:Signable):boolean {
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public fingerprint():string {
 | 
			
		||||
		return '';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MockConsumer implements SubConsumer {
 | 
			
		||||
 | 
			
		||||
	omnoms:	Array<string>
 | 
			
		||||
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.omnoms = Array<string>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public post(v:string) {
 | 
			
		||||
		this.omnoms.push(v);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MockPubSub implements PubSub {
 | 
			
		||||
 | 
			
		||||
	pubs:		Array<string>
 | 
			
		||||
	consumer:	SubConsumer
 | 
			
		||||
 | 
			
		||||
	constructor(name:string, consumer:SubConsumer) {
 | 
			
		||||
		this.pubs = Array<string>();	
 | 
			
		||||
		this.consumer = consumer;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public pub(v:string): boolean {
 | 
			
		||||
		this.pubs.push(v);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public flush() {
 | 
			
		||||
		while (this.pubs.length > 0) {
 | 
			
		||||
			const s = this.pubs.shift();
 | 
			
		||||
			this.consumer.post(s);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public close() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MockSignable implements Signable {
 | 
			
		||||
 | 
			
		||||
	src:	string
 | 
			
		||||
	dst:	string
 | 
			
		||||
 | 
			
		||||
	constructor(src:string) {
 | 
			
		||||
		this.src = src;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public digest():string {
 | 
			
		||||
		const h = crypto.createHash('sha256');
 | 
			
		||||
		h.update(this.src);
 | 
			
		||||
		this.dst= h.digest('hex');
 | 
			
		||||
		return this.dst;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MockKeyStore implements KeyStore {
 | 
			
		||||
 | 
			
		||||
	pk: any
 | 
			
		||||
	pubks: Array<any>
 | 
			
		||||
 | 
			
		||||
	constructor(pk:any, pubks:Array<any>) {
 | 
			
		||||
		this.pk = pk;
 | 
			
		||||
		this.pubks = pubks;	
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getPrivateKey(): any {
 | 
			
		||||
		return this.pk;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getTrustedKeys(): Array<any> {
 | 
			
		||||
		return this.pubks;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getTrustedActiveKeys(): Array<any> {
 | 
			
		||||
		return [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getEncryptKeys(): Array<any> {
 | 
			
		||||
		return [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getFingerprint(): string {
 | 
			
		||||
		return '';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	MockStore,
 | 
			
		||||
	MockPubSub,
 | 
			
		||||
	MockConsumer,
 | 
			
		||||
	MockSignable,
 | 
			
		||||
	MockKeyStore,
 | 
			
		||||
	MockSigner,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										241
									
								
								apps/cic-meta/tests/privatekeys.asc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								apps/cic-meta/tests/privatekeys.asc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,241 @@
 | 
			
		||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
1VrzRbOS5ymseDJcjejtMxuCOuSTN9R5KuQ=
 | 
			
		||||
=VqO+
 | 
			
		||||
-----END PGP PRIVATE KEY BLOCK-----
 | 
			
		||||
							
								
								
									
										151
									
								
								apps/cic-meta/tests/publickeys.asc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								apps/cic-meta/tests/publickeys.asc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
			
		||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
KuNFGWsKI1qTd1aEMRQzT4s+JJM=
 | 
			
		||||
=8257
 | 
			
		||||
-----END PGP PUBLIC KEY BLOCK-----
 | 
			
		||||
							
								
								
									
										317
									
								
								apps/cic-meta/tests/server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								apps/cic-meta/tests/server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,317 @@
 | 
			
		||||
import Automerge = require('automerge');
 | 
			
		||||
import assert = require('assert');
 | 
			
		||||
import fs = require('fs');
 | 
			
		||||
import pgp = require('openpgp');
 | 
			
		||||
import sqlite = require('sqlite3');
 | 
			
		||||
 | 
			
		||||
import * as handlers from '../scripts/server/handlers';
 | 
			
		||||
import { Envelope, Syncable, ArgPair } from '../src/sync';
 | 
			
		||||
import { PGPKeyStore, PGPSigner, KeyStore, Signer } from '../src/auth';
 | 
			
		||||
import { SqliteAdapter } from '../src/db';
 | 
			
		||||
 | 
			
		||||
function createKeystore() {
 | 
			
		||||
	const pksa = fs.readFileSync(__dirname + '/privatekeys.asc', 'utf-8');
 | 
			
		||||
	const pubksa = fs.readFileSync(__dirname + '/publickeys.asc', 'utf-8');
 | 
			
		||||
	return new Promise<PGPKeyStore>((whohoo, doh) => {
 | 
			
		||||
		let keystore = undefined;
 | 
			
		||||
		try {
 | 
			
		||||
			keystore = new PGPKeyStore('merman', pksa, pubksa, pubksa, pubksa, () => {
 | 
			
		||||
				whohoo(keystore);
 | 
			
		||||
			});
 | 
			
		||||
		} catch(e) {
 | 
			
		||||
			doh(e);
 | 
			
		||||
		}
 | 
			
		||||
		if (keystore === undefined) {
 | 
			
		||||
			doh();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createDatabase(sqlite_file:string):Promise<any> {
 | 
			
		||||
	try {
 | 
			
		||||
		fs.unlinkSync(sqlite_file);
 | 
			
		||||
	} catch {
 | 
			
		||||
	}
 | 
			
		||||
	return new Promise((whohoo, doh) => {
 | 
			
		||||
		//const db = new sqlite.Database(sqlite_file, (e) => {
 | 
			
		||||
		const dbconf = {
 | 
			
		||||
			name: sqlite_file,
 | 
			
		||||
			port: undefined,
 | 
			
		||||
			host: undefined,
 | 
			
		||||
			user: undefined,
 | 
			
		||||
			password: undefined,
 | 
			
		||||
		}
 | 
			
		||||
		const db = new SqliteAdapter(dbconf);//, (e) => {
 | 
			
		||||
//			if (e) {
 | 
			
		||||
//				doh(e);
 | 
			
		||||
//				return;
 | 
			
		||||
//			}
 | 
			
		||||
			const sql = `CREATE TABLE store (
 | 
			
		||||
id integer primary key autoincrement,
 | 
			
		||||
owner_fingerprint text not null,
 | 
			
		||||
hash char(64) not null unique,
 | 
			
		||||
content text not null
 | 
			
		||||
);
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
			console.log(sql);
 | 
			
		||||
			db.query(sql, (e) => {
 | 
			
		||||
				if (e) {
 | 
			
		||||
					doh(e);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				whohoo(db);
 | 
			
		||||
			});
 | 
			
		||||
//		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function wrap(s:Syncable, signer:Signer) {
 | 
			
		||||
	return new Promise<Envelope>((whohoo, doh) => {
 | 
			
		||||
		s.setSigner(signer);
 | 
			
		||||
		s.onwrap = async (env) => {
 | 
			
		||||
			if (env === undefined) {
 | 
			
		||||
				doh();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			whohoo(env);
 | 
			
		||||
		}
 | 
			
		||||
		s.sign();
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function signData(d:string, keyStore:KeyStore) {
 | 
			
		||||
	const digest = await pgp.message.fromText(d);
 | 
			
		||||
	const opts = {
 | 
			
		||||
		message: digest,
 | 
			
		||||
		privateKeys: [keyStore.getPrivateKey()],
 | 
			
		||||
		detached: true,
 | 
			
		||||
	};
 | 
			
		||||
	const signature = await pgp.sign(opts);
 | 
			
		||||
	return {
 | 
			
		||||
		data: signature.signature,
 | 
			
		||||
		engine: 'pgp',
 | 
			
		||||
		algo: 'sha256',
 | 
			
		||||
		digest: d,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('server', async () => {
 | 
			
		||||
	await it('put_client_then_retrieve', async () => {
 | 
			
		||||
		const keystore = await createKeystore();
 | 
			
		||||
 | 
			
		||||
		const signer = new PGPSigner(keystore);
 | 
			
		||||
 | 
			
		||||
		const digest = 'deadbeef';
 | 
			
		||||
		const s = new Syncable(digest, {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		
 | 
			
		||||
		const db = await createDatabase(__dirname + '/db.one.sqlite');
 | 
			
		||||
 | 
			
		||||
		let env = await wrap(s, signer);
 | 
			
		||||
		let j = env.toJSON();
 | 
			
		||||
		const content = await handlers.handleClientMergePut(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(content); // true-ish
 | 
			
		||||
 | 
			
		||||
		let v = await handlers.handleNoMergeGet(db, digest, keystore);
 | 
			
		||||
		if (v === undefined) {
 | 
			
		||||
			db.close();
 | 
			
		||||
			assert.fail('');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		v = await handlers.handleClientMergeGet(db, digest, keystore);
 | 
			
		||||
		if (v === undefined) {
 | 
			
		||||
			db.close();
 | 
			
		||||
			assert.fail('');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		db.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await it('client_merge', async () => {
 | 
			
		||||
		const keystore = await createKeystore();
 | 
			
		||||
		const signer = new PGPSigner(keystore);
 | 
			
		||||
 | 
			
		||||
		const db = await createDatabase(__dirname + '/db.two.sqlite');
 | 
			
		||||
 | 
			
		||||
		// create new, sign, wrap
 | 
			
		||||
		const digest = 'deadbeef';
 | 
			
		||||
		let s = new Syncable(digest, {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		await wrap(s, signer)
 | 
			
		||||
 | 
			
		||||
		// create client branch, sign, wrap, and serialize
 | 
			
		||||
		let update = new ArgPair('baz', 666)
 | 
			
		||||
		s.update([update], 'client branch');
 | 
			
		||||
		let env = await wrap(s, signer)
 | 
			
		||||
		const j_client = env.toJSON();
 | 
			
		||||
 | 
			
		||||
		// create server branch, sign, wrap, and serialize
 | 
			
		||||
		update = new ArgPair('baz', [1,2,3]);
 | 
			
		||||
		s.update([update], 'client branch');
 | 
			
		||||
		env = await wrap(s, signer)
 | 
			
		||||
		const j_server = env.toJSON();
 | 
			
		||||
 | 
			
		||||
		assert.notDeepEqual(j_client, j_server);
 | 
			
		||||
 | 
			
		||||
		let v = await handlers.handleClientMergePut(j_server, db, digest, keystore, signer);
 | 
			
		||||
		assert(v); // true-ish
 | 
			
		||||
 | 
			
		||||
		v = await handlers.handleClientMergePut(j_client, db, digest, keystore, signer);
 | 
			
		||||
		assert(v); // true-ish
 | 
			
		||||
		
 | 
			
		||||
		const j = await handlers.handleClientMergeGet(db, digest, keystore);
 | 
			
		||||
 | 
			
		||||
		env = Envelope.fromJSON(j);
 | 
			
		||||
		s = env.unwrap();
 | 
			
		||||
		
 | 
			
		||||
		db.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await it('server_merge', async () => {
 | 
			
		||||
		const keystore = await createKeystore();
 | 
			
		||||
		const signer = new PGPSigner(keystore);
 | 
			
		||||
 | 
			
		||||
		const db = await createDatabase(__dirname + '/db.three.sqlite');
 | 
			
		||||
 | 
			
		||||
		const digest = 'deadbeef';
 | 
			
		||||
		let s = new Syncable(digest, {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		let env = await wrap(s, signer)
 | 
			
		||||
		let j:any = env.toJSON();
 | 
			
		||||
 | 
			
		||||
		let v = await handlers.handleClientMergePut(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(v); // true-ish
 | 
			
		||||
 | 
			
		||||
		j = await handlers.handleNoMergeGet(db, digest, keystore);
 | 
			
		||||
		assert(v); // true-ish
 | 
			
		||||
 | 
			
		||||
		let o = JSON.parse(j);
 | 
			
		||||
		o.bar = 'xyzzy';
 | 
			
		||||
		j = JSON.stringify(o);
 | 
			
		||||
 | 
			
		||||
		let signMaterial = await handlers.handleServerMergePost(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(signMaterial)
 | 
			
		||||
 | 
			
		||||
		env = Envelope.fromJSON(signMaterial);
 | 
			
		||||
		const w = env.unwrap();
 | 
			
		||||
 | 
			
		||||
		console.log('jjjj', w, env);
 | 
			
		||||
 | 
			
		||||
		const signedData = await signData(w.m.signature.digest, keystore);
 | 
			
		||||
 | 
			
		||||
		o = {
 | 
			
		||||
			'm': env,
 | 
			
		||||
			's': signedData,
 | 
			
		||||
		}
 | 
			
		||||
		j = JSON.stringify(o);
 | 
			
		||||
 | 
			
		||||
		v = await handlers.handleServerMergePut(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(v);
 | 
			
		||||
 | 
			
		||||
		j = await handlers.handleNoMergeGet(db, digest, keystore);
 | 
			
		||||
		assert(j); // true-ish
 | 
			
		||||
		o = JSON.parse(j);
 | 
			
		||||
		console.log(o);
 | 
			
		||||
 | 
			
		||||
		db.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await it('server_merge', async () => {
 | 
			
		||||
		const keystore = await createKeystore();
 | 
			
		||||
		const signer = new PGPSigner(keystore);
 | 
			
		||||
 | 
			
		||||
		const db = await createDatabase(__dirname + '/db.three.sqlite');
 | 
			
		||||
 | 
			
		||||
		const digest = 'deadbeef';
 | 
			
		||||
		let s = new Syncable(digest, {
 | 
			
		||||
			bar: 'baz',
 | 
			
		||||
		});
 | 
			
		||||
		let env = await wrap(s, signer)
 | 
			
		||||
		let j:any = env.toJSON();
 | 
			
		||||
 | 
			
		||||
		let v = await handlers.handleClientMergePut(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(v); // true-ish
 | 
			
		||||
 | 
			
		||||
		j = await handlers.handleNoMergeGet(db, digest, keystore);
 | 
			
		||||
		assert(v); // true-ish
 | 
			
		||||
 | 
			
		||||
		let o = JSON.parse(j);
 | 
			
		||||
		o.bar = 'xyzzy';
 | 
			
		||||
		j = JSON.stringify(o);
 | 
			
		||||
 | 
			
		||||
		let signMaterial = await handlers.handleServerMergePost(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(signMaterial)
 | 
			
		||||
 | 
			
		||||
		env = Envelope.fromJSON(signMaterial);
 | 
			
		||||
 | 
			
		||||
		console.log('envvvv', env);
 | 
			
		||||
 | 
			
		||||
		const signedData = await signData(env.o['digest'], keystore);
 | 
			
		||||
		console.log('signed', signedData);
 | 
			
		||||
 | 
			
		||||
		o = {
 | 
			
		||||
			'm': env,
 | 
			
		||||
			's': signedData,
 | 
			
		||||
		}
 | 
			
		||||
		j = JSON.stringify(o);
 | 
			
		||||
		console.log(j);
 | 
			
		||||
 | 
			
		||||
		v = await handlers.handleServerMergePut(j, db, digest, keystore, signer);
 | 
			
		||||
		assert(v);
 | 
			
		||||
 | 
			
		||||
		j = await handlers.handleNoMergeGet(db, digest, keystore);
 | 
			
		||||
		assert(j); // true-ish
 | 
			
		||||
		o = JSON.parse(j);
 | 
			
		||||
		console.log(o);
 | 
			
		||||
 | 
			
		||||
		db.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//	await it('server_merge_empty', async () => {
 | 
			
		||||
//		const keystore = await createKeystore();
 | 
			
		||||
//		const signer = new PGPSigner(keystore);
 | 
			
		||||
//
 | 
			
		||||
//		const db = await createDatabase(__dirname + '/db.three.sqlite');
 | 
			
		||||
//
 | 
			
		||||
//		const digest = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
 | 
			
		||||
//		let o:any = {
 | 
			
		||||
//			foo: 'bar',
 | 
			
		||||
//			xyzzy: 42,
 | 
			
		||||
//		}
 | 
			
		||||
//		let j:any = JSON.stringify(o);
 | 
			
		||||
//
 | 
			
		||||
//		let signMaterial = await handlers.handleServerMergePost(j, db, digest, keystore, signer);
 | 
			
		||||
//		assert(signMaterial)
 | 
			
		||||
//
 | 
			
		||||
//		const env = Envelope.fromJSON(signMaterial);
 | 
			
		||||
//
 | 
			
		||||
//		console.log('envvvv', env);
 | 
			
		||||
//
 | 
			
		||||
//		const signedData = await signData(env.o['digest'], keystore);
 | 
			
		||||
//		console.log('signed', signedData);
 | 
			
		||||
//
 | 
			
		||||
//		o = {
 | 
			
		||||
//			'm': env,
 | 
			
		||||
//			's': signedData,
 | 
			
		||||
//		}
 | 
			
		||||
//		j = JSON.stringify(o);
 | 
			
		||||
//		console.log(j);
 | 
			
		||||
//
 | 
			
		||||
//		let v = await handlers.handleServerMergePut(j, db, digest, keystore, signer);
 | 
			
		||||
//		assert(v);
 | 
			
		||||
//
 | 
			
		||||
//		j = await handlers.handleNoMergeGet(db, digest, keystore);
 | 
			
		||||
//		assert(j); // true-ish
 | 
			
		||||
//		o = JSON.parse(j);
 | 
			
		||||
//		console.log(o);
 | 
			
		||||
//
 | 
			
		||||
//		db.close();
 | 
			
		||||
//	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								apps/cic-meta/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/cic-meta/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "outDir": "./dist.browser",
 | 
			
		||||
    "target": "es5",
 | 
			
		||||
    "module": "commonjs",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "lib": ["es2016", "dom", "es5"],
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "resolveJsonModule": true
 | 
			
		||||
  },
 | 
			
		||||
  "exclude": [
 | 
			
		||||
    "node_modules",
 | 
			
		||||
    "dist",
 | 
			
		||||
    "scripts",
 | 
			
		||||
    "tests"
 | 
			
		||||
  ],
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src/**/*",
 | 
			
		||||
    "scripts/server/*",
 | 
			
		||||
    "index.ts"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								apps/cic-meta/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/cic-meta/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
var webpack = require('webpack');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  entry: {
 | 
			
		||||
    index: './dist/index.js',
 | 
			
		||||
  },
 | 
			
		||||
  output: {
 | 
			
		||||
    path: path.resolve(__dirname, 'dist-web'),
 | 
			
		||||
    filename: 'cic-meta.web.js',
 | 
			
		||||
    library: 'cicMeta',
 | 
			
		||||
    libraryTarget: 'window'
 | 
			
		||||
  },
 | 
			
		||||
  mode: 'development',
 | 
			
		||||
  performance: {
 | 
			
		||||
    hints: false
 | 
			
		||||
  },
 | 
			
		||||
  stats: 'errors-only',
 | 
			
		||||
  resolve: {
 | 
			
		||||
	  fallback: {
 | 
			
		||||
		  "crypto": false,
 | 
			
		||||
	  },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user