/** * This file is part of Threema Web. * * Threema Web is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero * General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Threema Web. If not, see . */ import * as nacl from 'tweetnacl'; import {hexToU8a, u8aToHex} from '../helpers'; import {stringToUtf8a, utf8aToString} from '../helpers'; /** * This service stores trusted keys in the local browser storage. * * Data is encrypted as follows: * * plaintext = + + * [+ + ':' + ] * encrypted = nacl.secretbox(plaintext, , ) * * The data is encrypted using the first 32 bytes of the SHA512 hash of the * user defined password. The passwort should not be stored. * * The nonce is created randomly and stored alongside the encrypted password. * * Storage format: * * ":" * */ export class TrustedKeyStoreService { private STORAGE_KEY = 'trusted'; private logTag: string = '[TrustedKeyStoreService]'; private $log: ng.ILogService; private storage: Storage = null; public blocked = false; public static $inject = ['$log', '$window']; constructor($log: ng.ILogService, $window: ng.IWindowService) { this.$log = $log; try { if ($window.localStorage === null) { this.blocked = true; } this.storage = $window.localStorage; } catch (e) { $log.warn(this.logTag, 'LocalStorage blocked:', e); this.blocked = true; } } /** * Convert a string to an Uint8Array. * * This function is quite primitive, Unicode is not supported. */ private stringToBytes(str: string): Uint8Array { const arr = []; for (let i = 0; i < str.length; i++) { arr.push(str.charCodeAt(i)); } return new Uint8Array(arr); } /** * Convert a password string to a NaCl key. This is done by getting a * SHA512 hash and returning the first 32 bytes. */ private pwToKey(password: string): Uint8Array { const bytes = this.stringToBytes(password); const hash = nacl.hash(bytes); return hash.slice(0, nacl.secretbox.keyLength); } /** * Store the trusted key (and optionally the push token) in local browser * storage. Encrypt it using NaCl with the provided password. */ public storeTrustedKey(ownPublicKey: Uint8Array, ownSecretKey: Uint8Array, peerPublicKey: Uint8Array, pushToken: string | null, pushTokenType: threema.PushTokenType | null, password: string): void { const nonce: Uint8Array = nacl.randomBytes(nacl.secretbox.nonceLength); // Add prefix to push token string let pushTokenString = null; if (pushToken !== null && pushTokenType !== null) { switch (pushTokenType) { case threema.PushTokenType.Gcm: pushTokenString = threema.PushTokenPrefix.Gcm + ':' + pushToken; break; case threema.PushTokenType.Apns: pushTokenString = threema.PushTokenPrefix.Apns + ':' + pushToken; break; } } const token: Uint8Array = (pushTokenString == null) ? new Uint8Array(0) : stringToUtf8a(pushTokenString); const data = new Uint8Array(3 * 32 + token.byteLength); // TODO: Stop storing public key (redundant) data.set(ownPublicKey, 0); data.set(ownSecretKey, 32); data.set(peerPublicKey, 64); data.set(token, 96); const encrypted: Uint8Array = nacl.secretbox(data, nonce, this.pwToKey(password)); this.$log.debug(this.logTag, 'Storing trusted key'); this.storage.setItem(this.STORAGE_KEY, u8aToHex(nonce) + ':' + u8aToHex(encrypted)); } /** * Return whether or not a trusted key is stored in local storage. */ public hasTrustedKey(): boolean { const item: string = this.storage.getItem(this.STORAGE_KEY); return item !== null && item.length > 96 && item.indexOf(':') !== -1; } /** * Retrieve the trusted key from local browser storage. Decrypt it using * the provided password. */ public retrieveTrustedKey(password: string): threema.TrustedKeyStoreData | null { const storedValue: string = this.storage.getItem(this.STORAGE_KEY); if (storedValue === null) { return null; } const parts: string[] = storedValue.split(':'); if (parts.length !== 2) { return null; } const nonce = hexToU8a(parts[0]); const encrypted = hexToU8a(parts[1]); const decrypted = nacl.secretbox.open(encrypted, nonce, this.pwToKey(password)); if (!decrypted) { return null; } // Parse push token const tokenBytes = (decrypted as Uint8Array).slice(96); const tokenString: string | null = tokenBytes.byteLength > 0 ? utf8aToString(tokenBytes) : null; let tokenType: threema.PushTokenType = null; let token: string = null; if (tokenString !== null && tokenString[1] === ':') { switch (tokenString[0]) { case threema.PushTokenPrefix.Gcm: tokenType = threema.PushTokenType.Gcm; break; case threema.PushTokenPrefix.Apns: tokenType = threema.PushTokenType.Apns; break; default: this.$log.error(this.logTag, 'Invalid push token type:', tokenString[0]); return null; } token = tokenString.slice(2); } else if (tokenString !== null) { // Compat token = tokenString; tokenType = threema.PushTokenType.Gcm; } return { ownPublicKey: (decrypted as Uint8Array).slice(0, 32), ownSecretKey: (decrypted as Uint8Array).slice(32, 64), peerPublicKey: (decrypted as Uint8Array).slice(64, 96), pushToken: token, pushTokenType: tokenType, }; } /** * Delete any stored trusted keys. */ public clearTrustedKey(): void { this.$log.debug(this.logTag, 'Clearing trusted key'); this.storage.removeItem(this.STORAGE_KEY); } }