-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from vault12/full-nacl-implementation
Full nacl implementation
- Loading branch information
Showing
21 changed files
with
866 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.