Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vault12-integration-pt2 #7

Merged
merged 10 commits into from
Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
dmitry-salnikov marked this conversation as resolved.
Show resolved Hide resolved
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
dmitry-salnikov marked this conversation as resolved.
Show resolved Hide resolved
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) {
dmitry-salnikov marked this conversation as resolved.
Show resolved Hide resolved
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;
pavlo-liapin marked this conversation as resolved.
Show resolved Hide resolved
kind: ZaxMessageKind.plain;
}

Expand Down