Automatic session reset
This commit is contained in:
		
					parent
					
						
							
								fe187226bb
							
						
					
				
			
			
				commit
				
					
						98e7e65d25
					
				
			
		
					 26 changed files with 803 additions and 225 deletions
				
			
		|  | @ -1,10 +1,9 @@ | |||
| // Copyright 2015-2020 Signal Messenger, LLC
 | ||||
| // SPDX-License-Identifier: AGPL-3.0-only
 | ||||
| 
 | ||||
| /* global libsignal, textsecure, SignalProtocolStore */ | ||||
| /* global libsignal, textsecure */ | ||||
| 
 | ||||
| describe('MessageReceiver', () => { | ||||
|   textsecure.storage.impl = new SignalProtocolStore(); | ||||
|   const { WebSocket } = window; | ||||
|   const number = '+19999999999'; | ||||
|   const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE'; | ||||
|  | @ -12,6 +11,7 @@ describe('MessageReceiver', () => { | |||
|   const signalingKey = libsignal.crypto.getRandomBytes(32 + 20); | ||||
| 
 | ||||
|   before(() => { | ||||
|     localStorage.clear(); | ||||
|     window.WebSocket = MockSocket; | ||||
|     textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); | ||||
|     textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId); | ||||
|  | @ -19,94 +19,126 @@ describe('MessageReceiver', () => { | |||
|     textsecure.storage.put('signaling_key', signalingKey); | ||||
|   }); | ||||
|   after(() => { | ||||
|     localStorage.clear(); | ||||
|     window.WebSocket = WebSocket; | ||||
|   }); | ||||
| 
 | ||||
|   describe('connecting', () => { | ||||
|     const attrs = { | ||||
|       type: textsecure.protobuf.Envelope.Type.CIPHERTEXT, | ||||
|       source: number, | ||||
|       sourceUuid: uuid, | ||||
|       sourceDevice: deviceId, | ||||
|       timestamp: Date.now(), | ||||
|     }; | ||||
|     const websocketmessage = new textsecure.protobuf.WebSocketMessage({ | ||||
|       type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, | ||||
|       request: { verb: 'PUT', path: '/messages' }, | ||||
|     let attrs; | ||||
|     let websocketmessage; | ||||
| 
 | ||||
|     before(() => { | ||||
|       attrs = { | ||||
|         type: textsecure.protobuf.Envelope.Type.CIPHERTEXT, | ||||
|         source: number, | ||||
|         sourceUuid: uuid, | ||||
|         sourceDevice: deviceId, | ||||
|         timestamp: Date.now(), | ||||
|         content: libsignal.crypto.getRandomBytes(200), | ||||
|       }; | ||||
|       const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); | ||||
| 
 | ||||
|       websocketmessage = new textsecure.protobuf.WebSocketMessage({ | ||||
|         type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, | ||||
|         request: { verb: 'PUT', path: '/api/v1/message', body }, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     before(done => { | ||||
|       const signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); | ||||
| 
 | ||||
|       const aesKey = signalingKey.slice(0, 32); | ||||
|       const macKey = signalingKey.slice(32, 32 + 20); | ||||
| 
 | ||||
|       window.crypto.subtle | ||||
|         .importKey('raw', aesKey, { name: 'AES-CBC' }, false, ['encrypt']) | ||||
|         .then(key => { | ||||
|           const iv = libsignal.crypto.getRandomBytes(16); | ||||
|           window.crypto.subtle | ||||
|             .encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal) | ||||
|             .then(ciphertext => { | ||||
|               window.crypto.subtle | ||||
|                 .importKey( | ||||
|                   'raw', | ||||
|                   macKey, | ||||
|                   { name: 'HMAC', hash: { name: 'SHA-256' } }, | ||||
|                   false, | ||||
|                   ['sign'] | ||||
|                 ) | ||||
|                 .then(innerKey => { | ||||
|                   window.crypto.subtle | ||||
|                     .sign({ name: 'HMAC', hash: 'SHA-256' }, innerKey, signal) | ||||
|                     .then(mac => { | ||||
|                       const version = new Uint8Array([1]); | ||||
|                       const message = dcodeIO.ByteBuffer.concat([ | ||||
|                         version, | ||||
|                         iv, | ||||
|                         ciphertext, | ||||
|                         mac, | ||||
|                       ]); | ||||
|                       websocketmessage.request.body = message.toArrayBuffer(); | ||||
|                       done(); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('connects', done => { | ||||
|       const mockServer = new MockServer( | ||||
|         `ws://localhost:8080/v1/websocket/?login=${encodeURIComponent( | ||||
|           uuid | ||||
|         )}.1&password=password` | ||||
|       ); | ||||
|     it('generates light-session-reset event when it cannot decrypt', done => { | ||||
|       const mockServer = new MockServer('ws://localhost:8081/'); | ||||
| 
 | ||||
|       mockServer.on('connection', server => { | ||||
|         server.send(new Blob([websocketmessage.toArrayBuffer()])); | ||||
|         setTimeout(() => { | ||||
|           server.send(new Blob([websocketmessage.toArrayBuffer()])); | ||||
|         }, 1); | ||||
|       }); | ||||
| 
 | ||||
|       window.addEventListener('textsecure:message', ev => { | ||||
|         const signal = ev.proto; | ||||
|         const keys = Object.keys(attrs); | ||||
| 
 | ||||
|         for (let i = 0, max = keys.length; i < max; i += 1) { | ||||
|           const key = keys[i]; | ||||
|           assert.strictEqual(attrs[key], signal[key]); | ||||
|         } | ||||
|         assert.strictEqual(signal.message.body, 'hello'); | ||||
|         mockServer.close(); | ||||
| 
 | ||||
|         done(); | ||||
|       }); | ||||
| 
 | ||||
|       window.messageReceiver = new textsecure.MessageReceiver( | ||||
|       const messageReceiver = new textsecure.MessageReceiver( | ||||
|         'oldUsername', | ||||
|         'username', | ||||
|         'password', | ||||
|         'signalingKey' | ||||
|         // 'ws://localhost:8080',
 | ||||
|         // window,
 | ||||
|         'signalingKey', | ||||
|         { | ||||
|           serverTrustRoot: 'AAAAAAAA', | ||||
|         } | ||||
|       ); | ||||
| 
 | ||||
|       messageReceiver.addEventListener('light-session-reset', done()); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('methods', () => { | ||||
|     let messageReceiver; | ||||
|     let mockServer; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       // Necessary to populate the server property inside of MockSocket. Without it, we
 | ||||
|       //   crash when doing any number of things to a MockSocket instance.
 | ||||
|       mockServer = new MockServer('ws://localhost:8081'); | ||||
| 
 | ||||
|       messageReceiver = new textsecure.MessageReceiver( | ||||
|         'oldUsername', | ||||
|         'username', | ||||
|         'password', | ||||
|         'signalingKey', | ||||
|         { | ||||
|           serverTrustRoot: 'AAAAAAAA', | ||||
|         } | ||||
|       ); | ||||
|     }); | ||||
|     afterEach(() => { | ||||
|       mockServer.close(); | ||||
|     }); | ||||
| 
 | ||||
|     describe('#isOverHourIntoPast', () => { | ||||
|       it('returns false for now', () => { | ||||
|         assert.isFalse(messageReceiver.isOverHourIntoPast(Date.now())); | ||||
|       }); | ||||
|       it('returns false for 5 minutes ago', () => { | ||||
|         const fiveMinutesAgo = Date.now() - 5 * 60 * 1000; | ||||
|         assert.isFalse(messageReceiver.isOverHourIntoPast(fiveMinutesAgo)); | ||||
|       }); | ||||
|       it('returns true for 65 minutes ago', () => { | ||||
|         const sixtyFiveMinutesAgo = Date.now() - 65 * 60 * 1000; | ||||
|         assert.isTrue(messageReceiver.isOverHourIntoPast(sixtyFiveMinutesAgo)); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('#cleanupSessionResets', () => { | ||||
|       it('leaves empty object alone', () => { | ||||
|         window.storage.put('sessionResets', {}); | ||||
|         messageReceiver.cleanupSessionResets(); | ||||
|         const actual = window.storage.get('sessionResets'); | ||||
| 
 | ||||
|         const expected = {}; | ||||
|         assert.deepEqual(actual, expected); | ||||
|       }); | ||||
|       it('filters out any timestamp older than one hour', () => { | ||||
|         const startValue = { | ||||
|           one: Date.now() - 1, | ||||
|           two: Date.now(), | ||||
|           three: Date.now() - 65 * 60 * 1000, | ||||
|         }; | ||||
|         window.storage.put('sessionResets', startValue); | ||||
|         messageReceiver.cleanupSessionResets(); | ||||
|         const actual = window.storage.get('sessionResets'); | ||||
| 
 | ||||
|         const expected = window._.pick(startValue, ['one', 'two']); | ||||
|         assert.deepEqual(actual, expected); | ||||
|       }); | ||||
|       it('filters out falsey items', () => { | ||||
|         const startValue = { | ||||
|           one: 0, | ||||
|           two: false, | ||||
|           three: Date.now(), | ||||
|         }; | ||||
|         window.storage.put('sessionResets', startValue); | ||||
|         messageReceiver.cleanupSessionResets(); | ||||
|         const actual = window.storage.get('sessionResets'); | ||||
| 
 | ||||
|         const expected = window._.pick(startValue, ['three']); | ||||
|         assert.deepEqual(actual, expected); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Scott Nonnenberg
				Scott Nonnenberg