Skip to content

Commit

Permalink
Merge pull request #2 from vault12/full-nacl-implementation
Browse files Browse the repository at this point in the history
Full nacl implementation
  • Loading branch information
pavlo-liapin authored Dec 28, 2020
2 parents 4038196 + 133f196 commit 7ef7d23
Show file tree
Hide file tree
Showing 21 changed files with 866 additions and 86 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-empty-function": [
"error",
{ "allow" : ["constructors"] }
],
"indent": [
"error",
2
Expand Down
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ notifications:
slack:
secure: XcMOhiXe8KGF+l80cUdpdjNQe3xUM00Cj1qOM3Td8BubpwNIm0HzksbY0dQbtREw4vGY6OgA09J9EjbYGqykheNJ8DToy1VhGnC1WB6l1y96MnefHhL3TEKDEp3o67DDEwF9UIQTAzqHg5T57+IbhQ+ceYMJAvuDLCD2y6dTaxzSCt65J4nTprWx2WQ28hmXband0973VQ7Iy/oVFC7FDmOppr1E4E7MsmheBu91Sp1doTq9mD+VdJ6Hn9NxeEBYZq14WJn01w6WkIKDVYYkpzlBWSi6nXK6K4zhahKhFXa33egqnXRnyN3Am8QXqJHvOrjs3sLicTac8PQsIXfCEVDPgjnQRo28TekIACCMpS/hD226HEGJO+67V06LtZhoxk2V+OWyWGNt33pY5YYUtqsfSigJgXoCDUM/mFfEH0CRwVyMctHgu6liVo/l7V/5lgRJGjg86XMVOxiUvX8MkNFg5D8OsLEwq4wy0Mq3kKSsOERAuwbztYwcnLV3pXEXn2+RyW/gXPu3tIpLslHf1kqRcgSWt9RAnCub3CKIRTKYqf4jDi3Y6tX/5Ub1AevfyWr5ibaG/4x40Vka3NTJRvUoEOXWn6+bruPgowivLwehk8CAM6P5O7e8Zb3S1s41k1ZoSqzxQuHXYgGIojgOA9gIByaE4vE7XAkqzoBs3lU=
email: false
script:
- npm run lint
- npm run test
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config: Config.InitialOptions = {
statements: 90,
},
},
setupFiles: ['jest-localstorage-mock'],
preset: 'ts-jest',
testEnvironment: 'jsdom', // TODO: run tests through both 'node' and 'jsdom'
verbose: true
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
"main": "dist/glow.js",
"types": "dist/types/index.d.ts",
"dependencies": {
"js-sha256": "0.9.0",
"tweetnacl": "1.0.3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "4.8.2",
"@typescript-eslint/parser": "4.8.2",
"eslint": "7.14.0",
"jest": "26.6.3",
"jest-localstorage-mock": "2.4.4",
"ts-jest": "26.4.4",
"ts-node": "9.0.0",
"typescript": "4.1.2"
Expand Down
9 changes: 8 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
export const config = {

COMM_KEY_TAG: '__::commKey::__',
NONCE_TAG: '__nc',
STORAGE_ROOT: '.v2.stor.vlt12',
// Relay tokens, keys and hashes are 32 bytes
RELAY_TOKEN_LEN: 32,

RELAY_TOKEN_B64: 44,

// 5 min - Matched with config.x.relay.token_timeout
RELAY_TOKEN_TIMEOUT: 5 * 60 * 1000
RELAY_TOKEN_TIMEOUT: 5 * 60 * 1000,

// 15 min - Matched with config.x.relay.session_timeout
RELAY_SESSION_TIMEOUT: 15 * 60 * 1000
};
49 changes: 49 additions & 0 deletions src/crypto-storage/crypto-storage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CryptoStorage } from './crypto-storage';
import { LocalStorageDriver } from './local-storage.driver';
import { NaCl } from '../nacl/nacl';
import { NaClDriver } from '../nacl/nacl-driver.interface';

describe('CryptoStorage', () => {
let nacl: NaClDriver;
let storage: CryptoStorage;

beforeAll(async () => {
NaCl.setInstance();
nacl = NaCl.getInstance();
storage = await CryptoStorage.new(new LocalStorageDriver(), 'test');
});

it('ASCII string write/read', async () => {
const secretPlaintext = 'The quick brown fox jumps over the lazy dog';

await storage.save('secretPlaintext', secretPlaintext);
const restoredPlaintext = await storage.get('secretPlaintext');
expect(secretPlaintext).toBe(restoredPlaintext);
});

it('Complex serialized object write/read', async () => {
const secretObject = {
field1: 'string value', // String
field2: 101, // Number
field3: { // Object
inside: 'secret'
},
field4: [1, 2, 3, 'big', 'secrets'], // Array
field5: 'Kæmi ný öxi hér ykist þjófum nú bæði víl og ádrepa' // Unicode string in Icelandic
};

await storage.save('secretObject', secretObject);
const restoredObject = await storage.get('secretObject');
expect(JSON.stringify(secretObject)).toBe(JSON.stringify(restoredObject));
});

it('Remove items', async () => {
await storage.remove('secretPlaintext');
await storage.remove('secretObject');

const notRestoredPlaintext = await storage.get('secretPlaintext');
const notRestoredObject = await storage.get('secretObject');
expect(notRestoredPlaintext).toBeNull();
expect(notRestoredObject).toBeNull();
});
});
91 changes: 91 additions & 0 deletions src/crypto-storage/crypto-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NaCl } from '../nacl/nacl';
import { Utils } from '../utils/utils';
import { config } from '../config';
import { StorageDriver } from './storage-driver.interface';
import { NaClDriver } from '../nacl/nacl-driver.interface';

// TODO: add bulk operations to set(), get() and remove() pairs of values simultaneously

/* CryptoStorage is a handy wrapper around any storage that provides JavaScript interface,
that allows to store symmetrically encrypted serializable Javascript objects and primitives. */
export class CryptoStorage {
private driver?: StorageDriver;
private rootKey?: string;
private storageKey?: Uint8Array;
private nacl: NaClDriver;

private constructor(storageDriver: StorageDriver, rootKey?: string) {
const nacl = NaCl.getInstance();
this.nacl = nacl;
this.driver = storageDriver;
this.rootKey = rootKey ? `.${rootKey}${config.STORAGE_ROOT}` : config.STORAGE_ROOT;
}

static async new(storageDriver: StorageDriver, rootKey?: string): Promise<CryptoStorage> {
const storage = new CryptoStorage(storageDriver, rootKey);
storage.storageKey = await storage.nacl.random_bytes(storage.nacl.crypto_secretbox_KEYBYTES);
return storage;
}

async save(tag: string, data: unknown): Promise<boolean> {
if (!this.driver) {
throw new Error('Storage driver is not set');
}
if (!this.storageKey) {
throw new Error('Storage key is not set');
}
// Convert the data to JSON, then convert that string to a byte array
const input = JSON.stringify(data);
const encoded = await this.nacl.encode_utf8(input);
// For each item in the store we also generate and save its own nonce
const nonce = await this.nacl.crypto_secretbox_random_nonce();
const cipherText = await this.nacl.crypto_secretbox(encoded, nonce, this.storageKey);
// Save the cipher text and nonce
await this.driver.set(this.addPrefix(tag), Utils.toBase64(cipherText));
await this.driver.set(this.addNonceTag(tag), Utils.toBase64(nonce));
return true;
}

async get(tag: string): Promise<unknown> {
if (!this.driver) {
throw new Error('Storage driver is not set');
}
if (!this.storageKey) {
throw new Error('Storage key is not set');
}
// Get cipher text and nonce from the storage
const data = await this.driver.get(this.addPrefix(tag));
const nonce = await this.driver.get(this.addNonceTag(tag));
// Nothing to do without cipher text or nonce
if (!data || !nonce) {
return null;
}
const dataBinary = Utils.fromBase64(data);
const nonceBinary = Utils.fromBase64(nonce);
const source = await this.nacl.crypto_secretbox_open(dataBinary, nonceBinary, this.storageKey);
if (source) {
const decoded = await this.nacl.decode_utf8(source);
return JSON.parse(decoded);
} else {
throw new Error('crypto_secretbox_open: decryption error');
}
}

async remove(tag: string): Promise<boolean> {
if (!this.driver) {
throw new Error('Storage driver is not set');
}
await this.driver.remove(this.addPrefix(tag));
await this.driver.remove(this.addNonceTag(tag));
return true;
}

// Keys are tagged in the storage with a versioned prefix
private addPrefix(key: string): string {
return this.rootKey ? (key + this.rootKey) : key;
}

private addNonceTag(tag: string): string {
return this.addPrefix(`${config.NONCE_TAG}.${tag}`);
}
}
28 changes: 28 additions & 0 deletions src/crypto-storage/local-storage.driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { StorageDriver } from './storage-driver.interface';

export class LocalStorageDriver implements StorageDriver {
private rootTag: string;

constructor(root = 'storage.') {
this.rootTag = `__glow.${root}`;
}

async get(key: string): Promise<string | null> {
const item = localStorage.getItem(this.tag(key));
return item ? item : null;
}

async set(key: string, value: string | null): Promise<void> {
if (value !== null) {
localStorage.setItem(this.tag(key), value);
}
}

async remove(key: string): Promise<void> {
localStorage.removeItem(this.tag(key));
}

private tag(key: string) {
return `${this.rootTag}.${key}`;
}
}
5 changes: 5 additions & 0 deletions src/crypto-storage/storage-driver.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface StorageDriver {
get(key: string): Promise<string | null>;
set(key: string, value: string | null): Promise<void>;
remove(key: string): Promise<void>;
}
89 changes: 89 additions & 0 deletions src/keyring/keyring.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { KeyRing } from './keyring';
import { NaCl } from '../nacl/nacl';
import { NaClDriver } from '../nacl/nacl-driver.interface';
import { Keys } from '../keys/keys';
import { config } from '../config';
import { Utils } from '../utils/utils';

describe('Keyring', () => {
let nacl: NaClDriver;

beforeAll(() => {
NaCl.setInstance();
nacl = NaCl.getInstance();
});

it('add/remove guests', async () => {
const ring = await KeyRing.new('test1');

const keys1 = new Keys(await nacl.crypto_box_keypair());
const keys2 = new Keys(await nacl.crypto_box_keypair());

await ring.addGuest('Alice', keys1.publicKey);
expect(ring.getNumberOfGuests()).toBe(1);
expect(ring.getGuestKey('Alice')).toBeDefined();

await ring.addGuest('Bob', keys2.publicKey);
expect(ring.getNumberOfGuests()).toBe(2);
expect(ring.getGuestKey('Bob')).toBeDefined();

await ring.removeGuest('Alice');
expect(ring.getNumberOfGuests()).toBe(1);
expect(ring.getGuestKey('Alice')).toBeNull();
expect(ring.getGuestKey('Bob')).toBeDefined();
});

it('get tags and keys', async() => {
const ring = await KeyRing.new('test2');
const commKey = ring.getPubCommKey();
expect(typeof commKey).toBe('string');

const aliceKey = new Keys(await nacl.crypto_box_keypair());
await ring.addGuest('Alice', aliceKey.publicKey);
const hpk = Utils.toBase64(await nacl.h2(aliceKey.publicKey));
expect(ring.getTagByHpk(hpk)).not.toBeNull();
expect(ring.getTagByHpk('Bob')).toBeNull();
});

it('backup and restore', async () => {
const originalRing = await KeyRing.new('test3');
for (let i = 0; i < 10; i++) {
const keys = new Keys(await nacl.crypto_box_keypair());
await originalRing.addGuest(`keys${i}`, keys.publicKey);
}

const backup = await originalRing.backup();

const restored = await KeyRing.fromBackup('test4', backup);
const backedUpAgain = await restored.backup();

expect(originalRing.getPubCommKey()).toEqual(restored.getPubCommKey());
for (let i = 0; i < 10; i++) {
expect(originalRing.getGuestKey(`keys${i}`)).toEqual(restored.getGuestKey(`keys${i}`));
}
expect(originalRing.getHpk()).toEqual(restored.getHpk());

expect(backup).toBe(backedUpAgain);
});

it('temporary keys', async () => {
jest.useFakeTimers();
// mock config value
config.RELAY_SESSION_TIMEOUT = 100;
const ring = await KeyRing.new('test5');
const keys = new Keys(await nacl.crypto_box_keypair());
await ring.addTempGuest('temp', keys.publicKey);
// the key has to exist before we run the timer
expect(ring.getGuestKey('temp')).not.toBeNull();
// the key should not have expired yet
expect(ring.getTimeToGuestExpiration('temp')).toBeGreaterThan(0);

jest.runAllTimers();

expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);
// the key and timeout are erased
expect(ring.getNumberOfGuests()).toBe(0);
expect(ring.getTimeToGuestExpiration('temp')).toBe(0);
});
});
Loading

0 comments on commit 7ef7d23

Please sign in to comment.