UNPKG

8.72 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3Copyright 2017 New Vector Ltd
4Copyright 2018 New Vector Ltd
5Copyright 2019 The Matrix.org Foundation C.I.C.
6
7Licensed under the Apache License, Version 2.0 (the "License");
8you may not use this file except in compliance with the License.
9You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13Unless required by applicable law or agreed to in writing, software
14distributed under the License is distributed on an "AS IS" BASIS,
15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16See the License for the specific language governing permissions and
17limitations under the License.
18*/
19
20/**
21 * @module store/session/webstorage
22 */
23
24import * as utils from "../../utils";
25import {logger} from '../../logger';
26
27const DEBUG = false; // set true to enable console logging.
28const E2E_PREFIX = "session.e2e.";
29
30/**
31 * Construct a web storage session store, capable of storing account keys,
32 * session keys and access tokens.
33 * @constructor
34 * @param {WebStorage} webStore A web storage implementation, e.g.
35 * 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
36 * @throws if the supplied 'store' does not meet the Storage interface of the
37 * WebStorage API.
38 */
39export function WebStorageSessionStore(webStore) {
40 this.store = webStore;
41 if (!utils.isFunction(webStore.getItem) ||
42 !utils.isFunction(webStore.setItem) ||
43 !utils.isFunction(webStore.removeItem) ||
44 !utils.isFunction(webStore.key) ||
45 typeof(webStore.length) !== 'number'
46 ) {
47 throw new Error(
48 "Supplied webStore does not meet the WebStorage API interface",
49 );
50 }
51}
52
53WebStorageSessionStore.prototype = {
54 /**
55 * Remove the stored end to end account for the logged-in user.
56 */
57 removeEndToEndAccount: function() {
58 this.store.removeItem(KEY_END_TO_END_ACCOUNT);
59 },
60
61 /**
62 * Load the end to end account for the logged-in user.
63 * Note that the end-to-end account is now stored in the
64 * crypto store rather than here: this remains here so
65 * old sessions can be migrated out of the session store.
66 * @return {?string} Base64 encoded account.
67 */
68 getEndToEndAccount: function() {
69 return this.store.getItem(KEY_END_TO_END_ACCOUNT);
70 },
71
72 /**
73 * Retrieves the known devices for all users.
74 * @return {object} A map from user ID to map of device ID to keys for the device.
75 */
76 getAllEndToEndDevices: function() {
77 const prefix = keyEndToEndDevicesForUser('');
78 const devices = {};
79 for (let i = 0; i < this.store.length; ++i) {
80 const key = this.store.key(i);
81 const userId = key.substr(prefix.length);
82 if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key);
83 }
84 return devices;
85 },
86
87 getEndToEndDeviceTrackingStatus: function() {
88 return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
89 },
90
91 /**
92 * Get the sync token corresponding to the device list.
93 *
94 * @return {String?} token
95 */
96 getEndToEndDeviceSyncToken: function() {
97 return getJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN);
98 },
99
100 /**
101 * Removes all end to end device data from the store
102 */
103 removeEndToEndDeviceData: function() {
104 removeByPrefix(this.store, keyEndToEndDevicesForUser(''));
105 removeByPrefix(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
106 removeByPrefix(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN);
107 },
108
109 /**
110 * Retrieve the end-to-end sessions between the logged-in user and another
111 * device.
112 * @param {string} deviceKey The public key of the other device.
113 * @return {object} A map from sessionId to Base64 end-to-end session.
114 */
115 getEndToEndSessions: function(deviceKey) {
116 return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
117 },
118
119 /**
120 * Retrieve all end-to-end sessions between the logged-in user and other
121 * devices.
122 * @return {object} A map of {deviceKey -> {sessionId -> session pickle}}
123 */
124 getAllEndToEndSessions: function() {
125 const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions(''));
126 const results = {};
127 for (const k of deviceKeys) {
128 const unprefixedKey = k.substr(keyEndToEndSessions('').length);
129 results[unprefixedKey] = getJsonItem(this.store, k);
130 }
131 return results;
132 },
133
134 /**
135 * Remove all end-to-end sessions from the store
136 * This is used after migrating sessions awat from the sessions store.
137 */
138 removeAllEndToEndSessions: function() {
139 removeByPrefix(this.store, keyEndToEndSessions(''));
140 },
141
142 /**
143 * Retrieve a list of all known inbound group sessions
144 *
145 * @return {{senderKey: string, sessionId: string}}
146 */
147 getAllEndToEndInboundGroupSessionKeys: function() {
148 const prefix = E2E_PREFIX + 'inboundgroupsessions/';
149 const result = [];
150 for (let i = 0; i < this.store.length; i++) {
151 const key = this.store.key(i);
152 if (!key.startsWith(prefix)) {
153 continue;
154 }
155 // we can't use split, as the components we are trying to split out
156 // might themselves contain '/' characters. We rely on the
157 // senderKey being a (32-byte) curve25519 key, base64-encoded
158 // (hence 43 characters long).
159
160 result.push({
161 senderKey: key.substr(prefix.length, 43),
162 sessionId: key.substr(prefix.length + 44),
163 });
164 }
165 return result;
166 },
167
168 getEndToEndInboundGroupSession: function(senderKey, sessionId) {
169 const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
170 return this.store.getItem(key);
171 },
172
173 removeAllEndToEndInboundGroupSessions: function() {
174 removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/');
175 },
176
177 /**
178 * Get the end-to-end state for all rooms
179 * @return {object} roomId -> object with the end-to-end info for the room.
180 */
181 getAllEndToEndRooms: function() {
182 const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
183 const results = {};
184 for (const k of roomKeys) {
185 const unprefixedKey = k.substr(keyEndToEndRoom('').length);
186 results[unprefixedKey] = getJsonItem(this.store, k);
187 }
188 return results;
189 },
190
191 removeAllEndToEndRooms: function() {
192 removeByPrefix(this.store, keyEndToEndRoom(''));
193 },
194
195 setLocalTrustedBackupPubKey: function(pubkey) {
196 this.store.setItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY, pubkey);
197 },
198
199 // XXX: This store is deprecated really, but added this as a temporary
200 // thing until cross-signing lands.
201 getLocalTrustedBackupPubKey: function() {
202 return this.store.getItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY);
203 },
204};
205
206const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
207const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
208const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
209const KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY = E2E_PREFIX + "trusted_backup_pubkey";
210
211function keyEndToEndDevicesForUser(userId) {
212 return E2E_PREFIX + "devices/" + userId;
213}
214
215function keyEndToEndSessions(deviceKey) {
216 return E2E_PREFIX + "sessions/" + deviceKey;
217}
218
219function keyEndToEndInboundGroupSession(senderKey, sessionId) {
220 return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
221}
222
223function keyEndToEndRoom(roomId) {
224 return E2E_PREFIX + "rooms/" + roomId;
225}
226
227function getJsonItem(store, key) {
228 try {
229 // if the key is absent, store.getItem() returns null, and
230 // JSON.parse(null) === null, so this returns null.
231 return JSON.parse(store.getItem(key));
232 } catch (e) {
233 debuglog("Failed to get key %s: %s", key, e);
234 debuglog(e.stack);
235 }
236 return null;
237}
238
239function getKeysWithPrefix(store, prefix) {
240 const results = [];
241 for (let i = 0; i < store.length; ++i) {
242 const key = store.key(i);
243 if (key.startsWith(prefix)) results.push(key);
244 }
245 return results;
246}
247
248function removeByPrefix(store, prefix) {
249 const toRemove = [];
250 for (let i = 0; i < store.length; ++i) {
251 const key = store.key(i);
252 if (key.startsWith(prefix)) toRemove.push(key);
253 }
254 for (const key of toRemove) {
255 store.removeItem(key);
256 }
257}
258
259function debuglog() {
260 if (DEBUG) {
261 logger.log(...arguments);
262 }
263}