Skip to content

Commit

Permalink
Merge pull request #7 from vault12/vault12-integration-pt2
Browse files Browse the repository at this point in the history
vault12-integration-pt2
  • Loading branch information
lomchik authored Jul 6, 2021
2 parents ef2f313 + 78c4486 commit 627d00e
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 57 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "glow.ts",
"version": "0.1.0",
"version": "0.2.0",
"displayName": "Glow.ts",
"description": "Client library for interacting with Zax Cryptographic Relay Servers",
"keywords": [
Expand Down
5 changes: 2 additions & 3 deletions src/crypto-storage/crypto-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ export class CryptoStorage {

static setStorageDriver(driver: StorageDriver) {
if (this.storageDriver) {
throw new Error('[NaCl] NaCl driver has been already set, it is supposed to be set only once');
} else {
this.storageDriver = driver;
throw new Error('[CryptoStorage] StorageDriver has been already set, it is supposed to be set only once');
}
this.storageDriver = driver;
return true;
}

Expand Down
22 changes: 22 additions & 0 deletions src/crypto-storage/in-memory-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { StorageDriver } from './storage-driver.interface';

/**
* temporary created in-memory storage for testing purposes
*/
export class InMemoryStorage implements StorageDriver {
private storage: {[key: string]: any} = {};
get(key: string) {
return Promise.resolve(this.storage[key]);
}
set (key: string, value: any) {
this.storage[key] = value;
return Promise.resolve();
}
remove(key: string) {
delete this.storage[key];
return Promise.resolve();
}
reset() {
this.storage = {};
}
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import { StorageDriver } from './crypto-storage/storage-driver.interface.js';
import { LocalStorageDriver } from './crypto-storage/local-storage.driver.js';
import { Mailbox } from './mailbox/mailbox';
import { Relay } from './relay/relay';
import { ZaxMessageKind, ZaxTextMessage, ZaxFileMessage, ZaxPlainMessage, ZaxParsedMessage } from './zax.interface';
import {
ZaxMessageKind, ZaxTextMessage, ZaxFileMessage, ZaxPlainMessage, ZaxParsedMessage, FileStatusResponse
} from './zax.interface';
import { JsNaClDriver } from './nacl/js-nacl-driver';
import { Utils } from './utils/utils';
import { NaClDriver } from './nacl/nacl-driver.interface';

export {
NaCl,
Expand All @@ -24,6 +27,8 @@ export {
ZaxFileMessage,
ZaxPlainMessage,
ZaxParsedMessage,
FileStatusResponse,
NaClDriver,
JsNaClDriver,
Utils
};
9 changes: 0 additions & 9 deletions src/mailbox/mailbox.messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,6 @@ describe('Mailbox / Messages', () => {
expect(ttl).toBe(MessageStatusResponse.MissingKey); // the key is missing on the relay
});

it('send unencrypted message', async () => {
const token = await Alice.upload(testRelayURL, 'Bob', 'some unencrypted message', false);
expect(token.length).toBeGreaterThan(0);
const count = await Bob.count(testRelayURL);
expect(count).toBe(1);
const [ message ] = await Bob.download(testRelayURL);
expect(message.data).toBe('some unencrypted message');
});

it('should reconnect after token expiration timeout', async () => {
// using 'modern' breaks the test in Jest 27+ environment
// See https://jestjs.io/blog/2021/05/25/jest-27#flipping-defaults
Expand Down
4 changes: 2 additions & 2 deletions src/mailbox/mailbox.offline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ describe('Mailbox / Offline tests', () => {
expect(decoded2).toEqual(utfSource2);
});

it('encrypts raw binary data', async () => {
const message = await Alice.encodeMessage('Bob_mbx', new Uint8Array([1, 2, 3, 4]));
it('produces proper encrypted messages after encryption', async () => {
const message = await Alice.encodeMessage('Bob_mbx', '1234');
expect(message.nonce).toHaveLength(32);
expect(message.ctext).toHaveLength(28);
});
Expand Down
52 changes: 52 additions & 0 deletions src/mailbox/mailbox.transfer-messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { CryptoStorage } from '../crypto-storage/crypto-storage';
import { InMemoryStorage } from '../crypto-storage/in-memory-storage';
import { NaCl } from '../nacl/nacl';
import { testRelayURL } from '../tests.helper';
import { Mailbox } from './mailbox';


describe('Mailbox / Transfer Messages', () => {

let Alice: Mailbox;
let Bob: Mailbox;
const messages = [
JSON.stringify({object: 'message'}),
'some unencrypted message',
'special ;@@#2sd characters',
'кирилиця'
];
const encryptMessages = [true, false];
const storage = new InMemoryStorage();

beforeAll(() => {
NaCl.setDefaultInstance();
CryptoStorage.setStorageDriver(storage);
});

encryptMessages.forEach(encrypt => {
messages.forEach((msg) => {
describe(`transfer message ${JSON.stringify(msg)} ${(encrypt ? 'with' : 'without')} encryption`, () => {
beforeAll(async () => {
storage.reset();
Alice = await Mailbox.new('Alice');
Bob = await Mailbox.new('Bob');

await Alice.keyRing.addGuest('Bob', Bob.keyRing.getPubCommKey());
await Bob.keyRing.addGuest('Alice', Alice.keyRing.getPubCommKey());
});

it('upload', async () => {
const token = await Alice.upload(testRelayURL, 'Bob', msg, encrypt);
expect(token.length).toBeGreaterThan(0);
});

it('download', async () => {
const [ message ] = await Bob.download(testRelayURL);
expect(message.data).toEqual(msg);
Bob.delete(testRelayURL, [message.nonce]);
});
});
});
});

});
38 changes: 26 additions & 12 deletions src/mailbox/mailbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class Mailbox {
* send a plaintext message. Returns a token that can be used with `messageStatus` command to check
* the status of the message
*/
async upload(url: string, guestKey: string, message: any, encrypt = true): Promise<Base64> {
async upload(url: string, guestKey: string, message: string, encrypt = true): Promise<Base64> {
const relay = await this.prepareRelay(url);
const guestPk = this.getGuestKey(guestKey);
const payload = encrypt ? await this.encodeMessage(guestKey, message) : message;
Expand Down Expand Up @@ -142,7 +142,11 @@ export class Mailbox {
*/
private async parseFileMessage(message: ZaxRawMessage, senderTag: string) {
const { nonce, ctext, uploadID } = JSON.parse(message.data);
const data: FileUploadMetadata = await this.decodeMessage(senderTag, nonce, ctext);
const rawData = await this.decodeMessage(senderTag, nonce, ctext);
if (rawData === null) {
throw new Error('[Mailbox] Failed to decode file message');
}
const data = JSON.parse(rawData) as FileUploadMetadata;
return { data, time: message.time, senderTag, uploadID, nonce, kind: ZaxMessageKind.file } as ZaxFileMessage;
}

Expand Down Expand Up @@ -204,7 +208,7 @@ export class Mailbox {
const secretKey = await this.nacl.random_bytes(this.nacl.crypto_secretbox_KEYBYTES);
rawMetadata.skey = Utils.toBase64(secretKey);

const metadata = await this.encodeMessage(guest, rawMetadata);
const metadata = await this.encodeMessage(guest, JSON.stringify(rawMetadata));

const response = await this.runRelayCommand(relay, RelayCommand.startFileUpload, {
to: toHpk,
Expand Down Expand Up @@ -295,11 +299,11 @@ export class Mailbox {
const connectionData = await relay.openConnection();
const encryptedSignature = await this.encryptSignature(connectionData);

const messagesNumber = await relay.prove(await relay.encodeMessage({
const messagesNumber = await relay.prove(await relay.encodeMessage(JSON.stringify({
pub_key: this.keyRing.getPubCommKey(),
nonce: encryptedSignature.nonce,
ctext: encryptedSignature.ctext
}));
})));
return parseInt(messagesNumber, 10);
}

Expand Down Expand Up @@ -327,10 +331,11 @@ export class Mailbox {
/**
* Encrypts the payload of the command and sends it to a relay
*/
private async runRelayCommand(relay: Relay, command: RelayCommand, params?: any, ctext?: string): Promise<string[]> {
private async runRelayCommand(
relay: Relay, command: RelayCommand, params?: {[key:string]: any}, ctext?: string): Promise<string[]> {
params = { cmd: command, ...params };
const hpk = await this.keyRing.getHpk();
const message = await relay.encodeMessage(params);
const message = await relay.encodeMessage(JSON.stringify(params));
return await relay.runCmd(command, hpk, message, ctext);
}

Expand All @@ -346,24 +351,33 @@ export class Mailbox {
// ---------- Message encoding / decoding ----------

/**
* Encodes a free-form object `message` to the guest key of a guest already
* Encodes `message` to the guest key of a guest already
* added to the keyring
*/
async encodeMessage(guest: string, message: any): Promise<EncryptedMessage> {
async encodeMessage(guest: string, message: string): Promise<EncryptedMessage> {
const guestPk = this.getGuestKey(guest);
const privateKey = this.keyRing.getPrivateCommKey();

return await EncryptionHelper.encodeMessage(message, Utils.fromBase64(guestPk), Utils.fromBase64(privateKey));
return await EncryptionHelper.encodeMessage(
await this.nacl.encode_utf8(message), Utils.fromBase64(guestPk), Utils.fromBase64(privateKey));
}

/**
* Decodes a ciphertext from a guest key already in our keyring with this nonce
* @returns null if failed to decode
*/
async decodeMessage(guest: string, nonce: Base64, ctext: Base64): Promise<any> {
async decodeMessage(guest: string, nonce: Base64, ctext: Base64) {
const guestPk = this.getGuestKey(guest);
const privateKey = this.keyRing.getPrivateCommKey();
let uint8ArrayCtext: Uint8Array;
try {
uint8ArrayCtext = Utils.fromBase64(ctext);
} catch (err) {
// looks like ctext was not encoded
return null;
}

return await EncryptionHelper.decodeMessage(Utils.fromBase64(nonce), Utils.fromBase64(ctext),
return await EncryptionHelper.decodeMessage(Utils.fromBase64(nonce), uint8ArrayCtext,
Utils.fromBase64(guestPk), Utils.fromBase64(privateKey));
}

Expand Down
15 changes: 5 additions & 10 deletions src/nacl/encryption.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,26 @@ export class EncryptionHelper {
/**
* Encodes a binary message with `cryptobox`
*/
static async encodeMessage(message: any, pkTo: Uint8Array, skFrom: Uint8Array, nonceData?: number):
Promise<EncryptedMessage> {
static async encodeMessage(message: Uint8Array, pkTo: Uint8Array, skFrom: Uint8Array, nonceData?: number) {
const nacl = NaCl.getInstance();

if (!(message instanceof Uint8Array)) {
message = await nacl.encode_utf8(JSON.stringify(message));
}

const nonce = await this.makeNonce(nonceData);
const ctext = await nacl.crypto_box(message, nonce, pkTo, skFrom);
return {
nonce: Utils.toBase64(nonce),
ctext: Utils.toBase64(ctext)
};
} as EncryptedMessage;
}

/**
* Decodes a binary message with `cryptobox_open`
* @returns null if failed to decode
*/
static async decodeMessage(nonce: Uint8Array, ctext: Uint8Array, pkFrom: Uint8Array, skTo: Uint8Array): Promise<any> {
static async decodeMessage(nonce: Uint8Array, ctext: Uint8Array, pkFrom: Uint8Array, skTo: Uint8Array) {
const nacl = NaCl.getInstance();
const data = await nacl.crypto_box_open(ctext, nonce, pkFrom, skTo);
if (data) {
const utf8 = await nacl.decode_utf8(data);
return JSON.parse(utf8);
return await nacl.decode_utf8(data);
}

return data;
Expand Down
15 changes: 3 additions & 12 deletions src/nacl/js-nacl-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sha256 } from 'js-sha256';

import { NaClDriver } from './nacl-driver.interface';
import { Keypair } from './keypair.interface';
import { Utils } from '../utils/utils';


/**
Expand Down Expand Up @@ -77,21 +78,11 @@ export class JsNaClDriver implements NaClDriver {
// https://github.com/tonyg/js-nacl/blob/cc70775cfc9d68a04905ca65c7f179b33a18066e/nacl_cooked.js

async encode_latin1(data: string): Promise<Uint8Array> {
const result = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
const c = data.charCodeAt(i);
if ((c & 0xff) !== c) throw { message: 'Cannot encode string in Latin1', str: data };
result[i] = (c & 0xff);
}
return result;
return Utils.encode_latin1(data);
}

async decode_latin1(data: Uint8Array): Promise<string> {
const encoded = [];
for (let i = 0; i < data.length; i++) {
encoded.push(String.fromCharCode(data[i]));
}
return encoded.join('');
return Utils.decode_latin1(data);
}

async encode_utf8(data: string): Promise<Uint8Array> {
Expand Down
4 changes: 1 addition & 3 deletions src/nacl/nacl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ export class NaCl {
public static setInstance(driver: NaClDriver): boolean {
if (this.driverInstance) {
throw new Error('[NaCl] NaCl driver has been already set, it is supposed to be set only once');
} else {
// fallback to the default JS driver
this.driverInstance = driver;
}
this.driverInstance = driver;

return true;
}
Expand Down
10 changes: 7 additions & 3 deletions src/relay/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class Relay {
return result;
}

async encodeMessage(message: any): Promise<EncryptedMessage> {
async encodeMessage(message: string): Promise<EncryptedMessage> {
if (!this.publicKey) {
throw new Error('[Relay] No relay public key found, open the connection first');
}
Expand All @@ -125,7 +125,7 @@ export class Relay {
}

return await EncryptionHelper.encodeMessage(
message, this.publicKey, Utils.fromBase64(this.sessionKeys?.privateKey));
await this.nacl.encode_utf8(message), this.publicKey, Utils.fromBase64(this.sessionKeys?.privateKey));
}

async decodeMessage(nonce: Base64, ctext: Base64): Promise<any> {
Expand All @@ -137,8 +137,12 @@ export class Relay {
throw new Error('[Relay] No session key found, open the connection first');
}

return await EncryptionHelper.decodeMessage(Utils.fromBase64(nonce), Utils.fromBase64(ctext), relayPk,
const decodedData = await EncryptionHelper.decodeMessage(Utils.fromBase64(nonce), Utils.fromBase64(ctext), relayPk,
Utils.fromBase64(this.sessionKeys.privateKey));
if (decodedData === null) {
throw new Error('[Relay] failed to decode message');
}
return JSON.parse(decodedData);
}

// ---------- Low-level server request handling ----------
Expand Down
7 changes: 7 additions & 0 deletions src/utils/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ describe('Utils', () => {

expect(actual).toBe(expected);
});

['some message', 'special ;@@#2sd characters'].forEach(msg => {
it('do to and from base64 for ' + msg, () => {
const base64 = Utils.toBase64(Utils.encode_latin1(msg));
expect(Utils.decode_latin1(Utils.fromBase64(base64))).toEqual(msg);
});
});
});
3 changes: 2 additions & 1 deletion src/zax.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface ZaxTextMessage {
/**
* Decoded object after JSON.parse
*/
data: any;
data: string;
time: number;
senderTag: string;
nonce: Base64;
Expand All @@ -80,6 +80,7 @@ export interface ZaxPlainMessage {
time: number;
from: Base64;
nonce: Base64;
senderTag: undefined;
kind: ZaxMessageKind.plain;
}

Expand Down

0 comments on commit 627d00e

Please sign in to comment.