keystore.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import * as nacl from 'tweetnacl';
  18. import {hexToU8a, u8aToHex} from '../helpers';
  19. import {stringToUtf8a, utf8aToString} from '../helpers';
  20. /**
  21. * This service stores trusted keys in the local browser storage.
  22. *
  23. * Data is encrypted as follows:
  24. *
  25. * plaintext = <ownPubKey> + <ownSecKey> + <peerPubKey>
  26. * [+ <pushtoken-type-prefix> + ':' + <pushtoken>]
  27. * encrypted = nacl.secretbox(plaintext, <nonce>, <key>)
  28. *
  29. * The data is encrypted using the first 32 bytes of the SHA512 hash of the
  30. * user defined password. The passwort should not be stored.
  31. *
  32. * The nonce is created randomly and stored alongside the encrypted password.
  33. *
  34. * Storage format:
  35. *
  36. * "<nonceHexString>:<encryptedHexString>"
  37. *
  38. */
  39. export class TrustedKeyStoreService {
  40. private STORAGE_KEY = 'trusted';
  41. private logTag: string = '[TrustedKeyStoreService]';
  42. private $log: ng.ILogService;
  43. private storage: Storage = null;
  44. public blocked = false;
  45. public static $inject = ['$log', '$window'];
  46. constructor($log: ng.ILogService, $window: ng.IWindowService) {
  47. this.$log = $log;
  48. try {
  49. if ($window.localStorage === null) {
  50. this.blocked = true;
  51. }
  52. this.storage = $window.localStorage;
  53. } catch (e) {
  54. $log.warn(this.logTag, 'LocalStorage blocked:', e);
  55. this.blocked = true;
  56. }
  57. }
  58. /**
  59. * Convert a string to an Uint8Array.
  60. *
  61. * This function is quite primitive, Unicode is not supported.
  62. */
  63. private stringToBytes(str: string): Uint8Array {
  64. const arr = [];
  65. for (let i = 0; i < str.length; i++) {
  66. arr.push(str.charCodeAt(i));
  67. }
  68. return new Uint8Array(arr);
  69. }
  70. /**
  71. * Convert a password string to a NaCl key. This is done by getting a
  72. * SHA512 hash and returning the first 32 bytes.
  73. */
  74. private pwToKey(password: string): Uint8Array {
  75. const bytes = this.stringToBytes(password);
  76. const hash = nacl.hash(bytes);
  77. return hash.slice(0, nacl.secretbox.keyLength);
  78. }
  79. /**
  80. * Store the trusted key (and optionally the push token) in local browser
  81. * storage. Encrypt it using NaCl with the provided password.
  82. */
  83. public storeTrustedKey(ownPublicKey: Uint8Array, ownSecretKey: Uint8Array,
  84. peerPublicKey: Uint8Array,
  85. pushToken: string | null, pushTokenType: threema.PushTokenType | null,
  86. password: string): void {
  87. const nonce: Uint8Array = nacl.randomBytes(nacl.secretbox.nonceLength);
  88. // Add prefix to push token string
  89. let pushTokenString = null;
  90. if (pushToken !== null && pushTokenType !== null) {
  91. switch (pushTokenType) {
  92. case threema.PushTokenType.Gcm:
  93. pushTokenString = threema.PushTokenPrefix.Gcm + ':' + pushToken;
  94. break;
  95. case threema.PushTokenType.Apns:
  96. pushTokenString = threema.PushTokenPrefix.Apns + ':' + pushToken;
  97. break;
  98. }
  99. }
  100. const token: Uint8Array = (pushTokenString == null)
  101. ? new Uint8Array(0)
  102. : stringToUtf8a(pushTokenString);
  103. const data = new Uint8Array(3 * 32 + token.byteLength);
  104. // TODO: Stop storing public key (redundant)
  105. data.set(ownPublicKey, 0);
  106. data.set(ownSecretKey, 32);
  107. data.set(peerPublicKey, 64);
  108. data.set(token, 96);
  109. const encrypted: Uint8Array = nacl.secretbox(data, nonce, this.pwToKey(password));
  110. this.$log.debug(this.logTag, 'Storing trusted key');
  111. this.storage.setItem(this.STORAGE_KEY, u8aToHex(nonce) + ':' + u8aToHex(encrypted));
  112. }
  113. /**
  114. * Return whether or not a trusted key is stored in local storage.
  115. */
  116. public hasTrustedKey(): boolean {
  117. const item: string = this.storage.getItem(this.STORAGE_KEY);
  118. return item !== null && item.length > 96 && item.indexOf(':') !== -1;
  119. }
  120. /**
  121. * Retrieve the trusted key from local browser storage. Decrypt it using
  122. * the provided password.
  123. */
  124. public retrieveTrustedKey(password: string): threema.TrustedKeyStoreData | null {
  125. const storedValue: string = this.storage.getItem(this.STORAGE_KEY);
  126. if (storedValue === null) {
  127. return null;
  128. }
  129. const parts: string[] = storedValue.split(':');
  130. if (parts.length !== 2) {
  131. return null;
  132. }
  133. const nonce = hexToU8a(parts[0]);
  134. const encrypted = hexToU8a(parts[1]);
  135. const decrypted = nacl.secretbox.open(encrypted, nonce, this.pwToKey(password));
  136. if (!decrypted) {
  137. return null;
  138. }
  139. // Parse push token
  140. const tokenBytes = (decrypted as Uint8Array).slice(96);
  141. const tokenString: string | null = tokenBytes.byteLength > 0 ? utf8aToString(tokenBytes) : null;
  142. let tokenType: threema.PushTokenType = null;
  143. let token: string = null;
  144. if (tokenString !== null && tokenString[1] === ':') {
  145. switch (tokenString[0]) {
  146. case threema.PushTokenPrefix.Gcm:
  147. tokenType = threema.PushTokenType.Gcm;
  148. break;
  149. case threema.PushTokenPrefix.Apns:
  150. tokenType = threema.PushTokenType.Apns;
  151. break;
  152. default:
  153. this.$log.error(this.logTag, 'Invalid push token type:', tokenString[0]);
  154. return null;
  155. }
  156. token = tokenString.slice(2);
  157. } else if (tokenString !== null) {
  158. // Compat
  159. token = tokenString;
  160. tokenType = threema.PushTokenType.Gcm;
  161. }
  162. return {
  163. ownPublicKey: (decrypted as Uint8Array).slice(0, 32),
  164. ownSecretKey: (decrypted as Uint8Array).slice(32, 64),
  165. peerPublicKey: (decrypted as Uint8Array).slice(64, 96),
  166. pushToken: token,
  167. pushTokenType: tokenType,
  168. };
  169. }
  170. /**
  171. * Delete any stored trusted keys.
  172. */
  173. public clearTrustedKey(): void {
  174. this.$log.debug(this.logTag, 'Clearing trusted key');
  175. this.storage.removeItem(this.STORAGE_KEY);
  176. }
  177. }