├── .eslintignore ├── .gitignore ├── src ├── internal │ ├── index.ts │ ├── curve.ts │ └── crypto.ts ├── index.ts ├── __test-utils__ │ ├── custom-jest-environment.js │ └── utils.ts ├── signal-protocol-address.ts ├── session-lock.ts ├── helpers.ts ├── key-helper.ts ├── __test__ │ ├── signal-protocol-address.test.ts │ ├── key-helper.test.ts │ ├── session-lock.test.ts │ ├── fingerprint-generator.test.ts │ ├── crypto.test.ts │ ├── storage-type.ts │ ├── session-builder.test.ts │ ├── signal-protocol-store.test.ts │ └── session-cipher.test.ts ├── curve.ts ├── session-types.ts ├── fingerprint-generator.ts ├── types.ts ├── session-builder.ts ├── session-record.ts └── session-cipher.ts ├── prettier.config.js ├── jestconfig.json ├── .eslintrc.js ├── package.json ├── tsconfig.json ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './curve' 2 | export * from './crypto' 3 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | printWidth: 120, 4 | proseWrap: 'preserve', 5 | semi: false, 6 | trailingComma: 'es5', 7 | singleQuote: true, 8 | overrides: [ 9 | { 10 | files: '{*.js?(on),*.md,.prettierrc,.babelrc}', 11 | options: { 12 | tabWidth: 2, 13 | }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "roots": [ 4 | "/src" 5 | ], 6 | "transform": { 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | "testRegex": "(src/__test__/.*\\.test\\..*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 10 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], 11 | "testEnvironment": "./src/__test-utils__/custom-jest-environment.js" 12 | } 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Curve25519Wrapper } from '@privacyresearch/curve25519-typescript' 2 | import { Curve } from './curve' 3 | 4 | export * from './types' 5 | export * from './signal-protocol-address' 6 | export * from './key-helper' 7 | export * from './fingerprint-generator' 8 | export * from './session-builder' 9 | export * from './session-cipher' 10 | export * from './session-types' 11 | export * from './curve' 12 | 13 | import * as Internal from './internal' 14 | 15 | export { setWebCrypto, setCurve } from './internal' 16 | 17 | // returns a promise of something with the shape of the old libsignal 18 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 19 | export default async () => { 20 | const cw = await Curve25519Wrapper.create() 21 | 22 | return { 23 | Curve: new Curve(new Internal.Curve(cw)), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | 4 | parserOptions: { 5 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 6 | sourceType: 'module', // Allows for the use of imports 7 | }, 8 | 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 11 | 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 12 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 13 | ], 14 | 15 | rules: { 16 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 17 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/__test-utils__/custom-jest-environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | // __test-utils__/custom-jest-environment.js 3 | // Stolen from: https://github.com/ipfs/jest-environment-aegir/blob/master/src/index.js 4 | // Overcomes error from jest internals.. this thing: https://github.com/facebook/jest/issues/6248 5 | 'use strict' 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const NodeEnvironment = require('jest-environment-node') 9 | 10 | class XMLHttpRequest {} 11 | 12 | class MyEnvironment extends NodeEnvironment { 13 | constructor(config) { 14 | super( 15 | Object.assign({}, config, { 16 | globals: Object.assign({}, config.globals, { 17 | Uint32Array: Uint32Array, 18 | Uint16Array: Uint16Array, 19 | Uint8Array: Uint8Array, 20 | ArrayBuffer: ArrayBuffer, 21 | window: {}, 22 | XMLHttpRequest: XMLHttpRequest, 23 | }), 24 | }) 25 | ) 26 | } 27 | 28 | async setup() {} 29 | 30 | async teardown() {} 31 | } 32 | 33 | module.exports = MyEnvironment 34 | -------------------------------------------------------------------------------- /src/signal-protocol-address.ts: -------------------------------------------------------------------------------- 1 | import { SignalProtocolAddressType } from './' 2 | 3 | export class SignalProtocolAddress implements SignalProtocolAddressType { 4 | static fromString(s: string): SignalProtocolAddress { 5 | if (!s.match(/.*\.\d+/)) { 6 | throw new Error(`Invalid SignalProtocolAddress string: ${s}`) 7 | } 8 | const parts = s.split('.') 9 | return new SignalProtocolAddress(parts[0], parseInt(parts[1])) 10 | } 11 | 12 | private _name: string 13 | private _deviceId: number 14 | constructor(_name: string, _deviceId: number) { 15 | this._name = _name 16 | this._deviceId = _deviceId 17 | } 18 | 19 | // Readonly properties 20 | get name(): string { 21 | return this._name 22 | } 23 | 24 | get deviceId(): number { 25 | return this._deviceId 26 | } 27 | 28 | // Expose properties as fuynctions for compatibility 29 | getName(): string { 30 | return this._name 31 | } 32 | 33 | getDeviceId(): number { 34 | return this._deviceId 35 | } 36 | 37 | toString(): string { 38 | return `${this._name}.${this._deviceId}` 39 | } 40 | 41 | equals(other: SignalProtocolAddressType): boolean { 42 | return other.name === this._name && other.deviceId == this._deviceId 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/session-lock.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* 3 | * jobQueue manages multiple queues indexed by device to serialize 4 | * session io ops on the database. 5 | */ 6 | 7 | const jobQueue: { [k: string]: Promise } = {} 8 | 9 | export type JobType = () => Promise 10 | 11 | export class SessionLock { 12 | static errors: any[] = [] 13 | static _promises: Promise[] = [] 14 | static queueJobForNumber(id: string, runJob: JobType): Promise { 15 | const runPrevious = jobQueue[id] || Promise.resolve() 16 | const runCurrent = (jobQueue[id] = runPrevious.then(runJob, runJob)) 17 | const promise = runCurrent 18 | .then(function () { 19 | if (jobQueue[id] === runCurrent) { 20 | delete jobQueue[id] 21 | } 22 | }) 23 | .catch((e) => { 24 | // SessionLock callers should already have seen these errors on their own 25 | // Promise chains, but we need to handle them here too so we just save them 26 | // so callers can review them. 27 | SessionLock.errors.push(e) 28 | }) 29 | SessionLock._promises.push(promise) 30 | return runCurrent 31 | } 32 | 33 | static async clearQueue(): Promise { 34 | await Promise.all(SessionLock._promises) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@privacyresearch/libsignal-protocol-typescript", 3 | "version": "0.0.16", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "repository": "https://github.com/privacyresearchgroup/libsignal-protocol-typescript", 7 | "author": "Rolfe Schmidt ", 8 | "license": "GPL-3.0-only", 9 | "scripts": { 10 | "test": "jest --config jestconfig.json", 11 | "lint": "eslint -c .eslintrc.js '**/*.ts'", 12 | "format": "prettier '**/{*.{js?(on),ts?(x),md},.*.js?(on)}' --write --list-different --config prettier.config.js", 13 | "prepare": "yarn run build", 14 | "build": "tsc -d", 15 | "prepublishOnly": "yarn run lint", 16 | "preversion": "yarn run lint && yarn test", 17 | "version": "yarn run format && git add -A src", 18 | "postversion": "git push && git push --tags" 19 | }, 20 | "devDependencies": { 21 | "@types/base64-js": "^1.3.0", 22 | "@types/jest": "^27.0.2", 23 | "@typescript-eslint/eslint-plugin": "^4.32.0", 24 | "@typescript-eslint/parser": "^4.32.0", 25 | "eslint": "^7.32.0", 26 | "eslint-config-prettier": "^8.3.0", 27 | "eslint-plugin-prettier": "^4.0.0", 28 | "jest": "^27.2.3", 29 | "prettier": "^2.4.1", 30 | "ts-jest": "^27.0.5", 31 | "typescript": "^4.4.3" 32 | }, 33 | "dependencies": { 34 | "@privacyresearch/curve25519-typescript": "^0.0.12", 35 | "@privacyresearch/libsignal-protocol-protobuf-ts": "^0.0.9", 36 | "base64-js": "^1.5.1" 37 | }, 38 | "files": [ 39 | "lib/*.js", 40 | "lib/*.d.ts", 41 | "lib/internal/**/*" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function arrayBufferToString(b: ArrayBuffer): string { 2 | return uint8ArrayToString(new Uint8Array(b)) 3 | } 4 | 5 | export function uint8ArrayToString(arr: Uint8Array): string { 6 | const end = arr.length 7 | let begin = 0 8 | if (begin === end) return '' 9 | let chars: number[] = [] 10 | const parts: string[] = [] 11 | while (begin < end) { 12 | chars.push(arr[begin++]) 13 | if (chars.length >= 1024) { 14 | parts.push(String.fromCharCode(...chars)) 15 | chars = [] 16 | } 17 | } 18 | return parts.join('') + String.fromCharCode(...chars) 19 | } 20 | export function binaryStringToArrayBuffer(str: string): ArrayBuffer { 21 | let i = 0 22 | const k = str.length 23 | let charCode 24 | const bb: number[] = [] 25 | while (i < k) { 26 | charCode = str.charCodeAt(i) 27 | if (charCode > 0xff) throw RangeError('illegal char code: ' + charCode) 28 | bb[i++] = charCode 29 | } 30 | return Uint8Array.from(bb).buffer 31 | } 32 | 33 | export function isEqual(a: ArrayBuffer | undefined, b: ArrayBuffer | undefined): boolean { 34 | // TODO: Special-case arraybuffers, etc 35 | if (a === undefined || b === undefined) { 36 | return false 37 | } 38 | const a1: string = arrayBufferToString(a) 39 | const b1: string = arrayBufferToString(b) 40 | const maxLength = Math.max(a1.length, b1.length) 41 | if (maxLength < 5) { 42 | throw new Error('a/b compare too short') 43 | } 44 | return a1.substring(0, Math.min(maxLength, a1.length)) == b1.substring(0, Math.min(maxLength, b1.length)) 45 | } 46 | 47 | export function uint8ArrayToArrayBuffer(arr: Uint8Array): ArrayBuffer { 48 | return arr.buffer.slice(arr.byteOffset, arr.byteLength + arr.byteOffset) 49 | } 50 | -------------------------------------------------------------------------------- /src/key-helper.ts: -------------------------------------------------------------------------------- 1 | import * as Internal from './internal' 2 | import { KeyPairType, SignedPreKeyPairType, PreKeyPairType } from './types' 3 | 4 | export class KeyHelper { 5 | static generateIdentityKeyPair(): Promise { 6 | return Internal.crypto.createKeyPair() 7 | } 8 | 9 | static generateRegistrationId(): number { 10 | const registrationId = new Uint16Array(Internal.crypto.getRandomBytes(2))[0] 11 | return registrationId & 0x3fff 12 | } 13 | 14 | static async generateSignedPreKey( 15 | identityKeyPair: KeyPairType, 16 | signedKeyId: number 17 | ): Promise { 18 | if ( 19 | !(identityKeyPair.privKey instanceof ArrayBuffer) || 20 | identityKeyPair.privKey.byteLength !== 32 || 21 | !(identityKeyPair.pubKey instanceof ArrayBuffer) || 22 | identityKeyPair.pubKey.byteLength !== 33 23 | ) { 24 | throw new TypeError('Invalid argument for identityKeyPair') 25 | } 26 | if (!isNonNegativeInteger(signedKeyId)) { 27 | throw new TypeError('Invalid argument for signedKeyId: ' + signedKeyId) 28 | } 29 | const keyPair = await Internal.crypto.createKeyPair() 30 | const sig = await Internal.crypto.Ed25519Sign(identityKeyPair.privKey, keyPair.pubKey) 31 | return { 32 | keyId: signedKeyId, 33 | keyPair: keyPair, 34 | signature: sig, 35 | } 36 | } 37 | 38 | static async generatePreKey(keyId: number): Promise { 39 | if (!isNonNegativeInteger(keyId)) { 40 | throw new TypeError('Invalid argument for keyId: ' + keyId) 41 | } 42 | 43 | const keyPair = await Internal.crypto.createKeyPair() 44 | return { keyId: keyId, keyPair: keyPair } 45 | } 46 | } 47 | 48 | function isNonNegativeInteger(n: unknown): n is number { 49 | return typeof n === 'number' && n % 1 === 0 && n >= 0 50 | } 51 | -------------------------------------------------------------------------------- /src/__test__/signal-protocol-address.test.ts: -------------------------------------------------------------------------------- 1 | import { SignalProtocolAddress } from '../signal-protocol-address' 2 | 3 | describe('SignalProtocolAddress', function () { 4 | const name = 'name' 5 | const deviceId = 42 6 | const serialized = 'name.42' 7 | 8 | describe('getName', function () { 9 | test('returns the name', () => { 10 | const address = new SignalProtocolAddress(name, 1) 11 | expect(address.getName()).toBe(name) 12 | expect(address.name).toBe(name) 13 | }) 14 | }) 15 | 16 | describe('getDeviceId', function () { 17 | test('returns the deviceId', () => { 18 | const address = new SignalProtocolAddress(name, deviceId) 19 | expect(address.getDeviceId()).toBe(deviceId) 20 | expect(address.deviceId).toBe(deviceId) 21 | }) 22 | }) 23 | 24 | describe('toString', function () { 25 | test('returns the address', () => { 26 | const address = new SignalProtocolAddress(name, deviceId) 27 | expect(address.toString()).toBe(serialized) 28 | }) 29 | }) 30 | describe('fromString', function () { 31 | test('throws on a bad inputs', () => { 32 | const bads = ['', null, {}] 33 | for (const bad of bads) { 34 | expect(() => { 35 | // We are testing data that Typescript wouldn't allow 36 | // because Javascript users might send it. 37 | SignalProtocolAddress.fromString(bad as string) 38 | }).toThrow() 39 | } 40 | }) 41 | 42 | test('constructs the address', () => { 43 | const address = SignalProtocolAddress.fromString(serialized) 44 | expect(address.getDeviceId()).toBe(deviceId) 45 | expect(address.deviceId).toBe(deviceId) 46 | expect(address.getName()).toBe(name) 47 | expect(address.name).toBe(name) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/curve.ts: -------------------------------------------------------------------------------- 1 | import * as Internal from './internal' 2 | import { KeyPairType, AsyncCurveType } from './types' 3 | 4 | export class Curve { 5 | private _curve: Internal.Curve 6 | async: AsyncCurve 7 | constructor(curve: Internal.Curve) { 8 | this._curve = curve 9 | this.async = new AsyncCurve(curve.async) 10 | } 11 | 12 | generateKeyPair(): KeyPairType { 13 | const privKey = Internal.crypto.getRandomBytes(32) 14 | return this._curve.createKeyPair(privKey) 15 | } 16 | createKeyPair(privKey: ArrayBuffer): KeyPairType { 17 | return this._curve.createKeyPair(privKey) 18 | } 19 | calculateAgreement(pubKey: ArrayBuffer, privKey: ArrayBuffer): ArrayBuffer { 20 | return this._curve.ECDHE(pubKey, privKey) 21 | } 22 | verifySignature(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): boolean { 23 | return this._curve.Ed25519Verify(pubKey, msg, sig) 24 | } 25 | calculateSignature(privKey: ArrayBuffer, message: ArrayBuffer): ArrayBuffer { 26 | return this._curve.Ed25519Sign(privKey, message) 27 | } 28 | } 29 | 30 | export class AsyncCurve implements AsyncCurveType { 31 | private _curve: Internal.AsyncCurve 32 | constructor(curve: Internal.AsyncCurve) { 33 | this._curve = curve 34 | } 35 | 36 | generateKeyPair(): Promise { 37 | const privKey = Internal.crypto.getRandomBytes(32) 38 | return this._curve.createKeyPair(privKey) 39 | } 40 | 41 | createKeyPair(privKey: ArrayBuffer): Promise { 42 | return this._curve.createKeyPair(privKey) 43 | } 44 | 45 | calculateAgreement(pubKey: ArrayBuffer, privKey: ArrayBuffer): Promise { 46 | return this._curve.ECDHE(pubKey, privKey) 47 | } 48 | 49 | verifySignature(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): Promise { 50 | return this._curve.Ed25519Verify(pubKey, msg, sig) 51 | } 52 | 53 | calculateSignature(privKey: ArrayBuffer, message: ArrayBuffer): Promise { 54 | return this._curve.Ed25519Sign(privKey, message) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/session-types.ts: -------------------------------------------------------------------------------- 1 | import { KeyPairType, SignedPublicPreKeyType, PreKeyType } from './types' 2 | 3 | export enum ChainType { 4 | SENDING = 1, 5 | RECEIVING = 2, 6 | } 7 | 8 | export enum BaseKeyType { 9 | OURS = 1, 10 | THEIRS = 2, 11 | } 12 | 13 | export interface SessionType { 14 | indexInfo: IndexInfo 15 | registrationId?: number 16 | currentRatchet: Ratchet 17 | pendingPreKey?: PendingPreKey 18 | 19 | oldRatchetList: OldRatchetInfo[] 20 | 21 | chains: { [ephKeyString: string]: Chain } 22 | } 23 | 24 | export interface IndexInfo { 25 | closed: number 26 | remoteIdentityKey: T 27 | baseKey?: T 28 | baseKeyType?: BaseKeyType 29 | } 30 | 31 | export interface Ratchet { 32 | rootKey: T 33 | ephemeralKeyPair?: KeyPairType 34 | lastRemoteEphemeralKey: T 35 | previousCounter: number 36 | added?: number //timestamp 37 | } 38 | export interface OldRatchetInfo { 39 | ephemeralKey: T 40 | added: number //timestamp 41 | } 42 | 43 | export interface Chain { 44 | chainType: ChainType 45 | chainKey: { key?: T; counter: number } 46 | messageKeys: { [key: number]: T } 47 | } 48 | 49 | export interface PendingPreKey { 50 | baseKey: T 51 | preKeyId?: number 52 | signedKeyId: number 53 | } 54 | 55 | export enum EncryptionResultMessageType { 56 | WhisperMessage = 1, 57 | PreKeyWhisperMessage = 3, 58 | } 59 | 60 | export interface EncryptionResult { 61 | type: EncryptionResultMessageType 62 | body: ArrayBuffer 63 | registrationId: number 64 | } 65 | 66 | export interface DeviceType { 67 | identityKey: T 68 | signedPreKey: SignedPublicPreKeyType 69 | preKey?: PreKeyType 70 | registrationId?: number 71 | } 72 | 73 | export interface RecordType { 74 | archiveCurrentState: () => void 75 | deleteAllSessions: () => void 76 | getOpenSession: () => SessionType | undefined 77 | getSessionByBaseKey: (baseKey: ArrayBuffer) => SessionType | undefined 78 | getSessionByRemoteEphemeralKey: (remoteEphemeralKey: ArrayBuffer) => SessionType | undefined 79 | getSessions: () => SessionType[] 80 | haveOpenSession: () => boolean 81 | promoteState: (session: SessionType) => void 82 | serialize: () => string 83 | updateSessionState: (session: SessionType) => void 84 | } 85 | -------------------------------------------------------------------------------- /src/__test__/key-helper.test.ts: -------------------------------------------------------------------------------- 1 | import { KeyPairType } from '../types' 2 | import { KeyHelper } from '../key-helper' 3 | 4 | import * as Internal from '../internal' 5 | 6 | describe('KeyHelper', function () { 7 | function validateKeyPair(keyPair: KeyPairType): void { 8 | expect(keyPair.pubKey).toBeDefined() 9 | expect(keyPair.privKey).toBeDefined() 10 | expect(keyPair.privKey.byteLength).toStrictEqual(32) 11 | expect(keyPair.pubKey.byteLength).toStrictEqual(33) 12 | expect(new Uint8Array(keyPair.pubKey)[0]).toStrictEqual(5) 13 | } 14 | 15 | describe('generateIdentityKeyPair', function () { 16 | test(`works`, async () => { 17 | const keyPair = await KeyHelper.generateIdentityKeyPair() 18 | validateKeyPair(keyPair) 19 | }) 20 | }) 21 | 22 | describe('generateRegistrationId', function () { 23 | test(`works`, () => { 24 | const registrationId = KeyHelper.generateRegistrationId() 25 | expect(typeof registrationId).toBe('number') 26 | expect(registrationId).toBeGreaterThanOrEqual(0) 27 | expect(registrationId).toBeLessThan(16384) 28 | expect(registrationId).toStrictEqual(Math.round(registrationId)) 29 | }) 30 | }) 31 | 32 | describe('generatePreKey', function () { 33 | test(`generates a PreKey`, async () => { 34 | const pk = await KeyHelper.generatePreKey(1337) 35 | validateKeyPair(pk.keyPair) 36 | expect(pk.keyId).toStrictEqual(1337) 37 | }) 38 | 39 | test(`throws on bad ID`, async () => { 40 | await expect(async () => { 41 | await KeyHelper.generatePreKey(-7) 42 | }).rejects.toThrow() 43 | }) 44 | }) 45 | 46 | describe('generateSignedPreKey', function () { 47 | test(`generates a PreKey`, async () => { 48 | const identityKey = await KeyHelper.generateIdentityKeyPair() 49 | 50 | const spk = await KeyHelper.generateSignedPreKey(identityKey, 1337) 51 | validateKeyPair(spk.keyPair) 52 | expect(spk.keyId).toStrictEqual(1337) 53 | await expect( 54 | Internal.crypto.Ed25519Verify(identityKey.pubKey, spk.keyPair.pubKey, spk.signature) 55 | ).resolves.toBe(false) 56 | }) 57 | 58 | test(`throws on bad ID`, async () => { 59 | const identityKey = await KeyHelper.generateIdentityKeyPair() 60 | await expect(async () => { 61 | await KeyHelper.generateSignedPreKey(identityKey, -7) 62 | }).rejects.toThrow() 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/__test-utils__/utils.ts: -------------------------------------------------------------------------------- 1 | import { DeviceType } from '..' 2 | import { KeyHelper } from '../key-helper' 3 | import { SignalProtocolStore } from '../__test__/storage-type' 4 | 5 | export function hexToArrayBuffer(str: string): ArrayBuffer { 6 | const ret = new ArrayBuffer(str.length / 2) 7 | const array = new Uint8Array(ret) 8 | for (let i = 0; i < str.length / 2; i++) array[i] = parseInt(str.substr(i * 2, 2), 16) 9 | return ret 10 | } 11 | 12 | export function assertEqualArrayBuffers(ab1: ArrayBuffer, ab2: ArrayBuffer): void { 13 | const a1 = new Uint8Array(ab1) 14 | const a2 = new Uint8Array(ab2) 15 | expect(a1.length).toBe(a2.length) 16 | for (let i = 0; i < a1.length; ++i) { 17 | expect(a1[i]).toBe(a2[i]) 18 | } 19 | } 20 | 21 | export function assertEqualUint8Arrays(a1: Uint8Array, a2: Uint8Array): void { 22 | expect(a1.length).toBe(a2.length) 23 | for (let i = 0; i < a1.length; ++i) { 24 | expect(a1[i]).toBe(a2[i]) 25 | } 26 | } 27 | 28 | export async function generateIdentity(store: SignalProtocolStore): Promise { 29 | return Promise.all([KeyHelper.generateIdentityKeyPair(), KeyHelper.generateRegistrationId()]).then(function ( 30 | result 31 | ) { 32 | store.put('identityKey', result[0]) 33 | store.put('registrationId', result[1]) 34 | }) 35 | } 36 | 37 | export async function generatePreKeyBundle( 38 | store: SignalProtocolStore, 39 | preKeyId: number, 40 | signedPreKeyId: number 41 | ): Promise> { 42 | return Promise.all([store.getIdentityKeyPair(), store.getLocalRegistrationId()]).then(function (result) { 43 | const identity = result[0] 44 | const registrationId = result[1] 45 | 46 | return Promise.all([ 47 | KeyHelper.generatePreKey(preKeyId), 48 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 49 | KeyHelper.generateSignedPreKey(identity!, signedPreKeyId), 50 | ]).then(function (keys) { 51 | const preKey = keys[0] 52 | const signedPreKey = keys[1] 53 | 54 | store.storePreKey(preKeyId, preKey.keyPair) 55 | store.storeSignedPreKey(signedPreKeyId, signedPreKey.keyPair) 56 | 57 | return { 58 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 59 | identityKey: identity!.pubKey, 60 | registrationId: registrationId, 61 | preKey: { 62 | keyId: preKeyId, 63 | publicKey: preKey.keyPair.pubKey, 64 | }, 65 | signedPreKey: { 66 | keyId: signedPreKeyId, 67 | publicKey: signedPreKey.keyPair.pubKey, 68 | signature: signedPreKey.signature, 69 | }, 70 | } 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/fingerprint-generator.ts: -------------------------------------------------------------------------------- 1 | import { FingerprintGeneratorType } from './' 2 | import * as utils from './helpers' 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const msrcrypto = require('../lib/msrcrypto') 5 | 6 | export class FingerprintGenerator implements FingerprintGeneratorType { 7 | static VERSION = 0 8 | 9 | async createFor( 10 | localIdentifier: string, 11 | localIdentityKey: ArrayBuffer, 12 | remoteIdentifier: string, 13 | remoteIdentityKey: ArrayBuffer 14 | ): Promise { 15 | const localStr = await getDisplayStringFor(localIdentifier, localIdentityKey, this._iterations) 16 | const remoteStr = await getDisplayStringFor(remoteIdentifier, remoteIdentityKey, this._iterations) 17 | return [localStr, remoteStr].sort().join('') 18 | } 19 | 20 | private _iterations: number 21 | constructor(_iterations: number) { 22 | this._iterations = _iterations 23 | } 24 | } 25 | 26 | async function getDisplayStringFor(identifier: string, key: ArrayBuffer, iterations: number): Promise { 27 | const bytes = concatArrayBuffers([ 28 | shortToArrayBuffer(FingerprintGenerator.VERSION), 29 | key, 30 | utils.binaryStringToArrayBuffer(identifier), 31 | ]) 32 | 33 | const hash = await iterateHash(bytes, key, iterations) 34 | const output = new Uint8Array(hash) 35 | return ( 36 | getEncodedChunk(output, 0) + 37 | getEncodedChunk(output, 5) + 38 | getEncodedChunk(output, 10) + 39 | getEncodedChunk(output, 15) + 40 | getEncodedChunk(output, 20) + 41 | getEncodedChunk(output, 25) 42 | ) 43 | } 44 | 45 | async function iterateHash(data: ArrayBuffer, key: ArrayBuffer, count: number): Promise { 46 | const data1 = concatArrayBuffers([data, key]) 47 | const result = await msrcrypto.subtle.digest({ name: 'SHA-512' }, data1) 48 | 49 | if (--count === 0) { 50 | return result 51 | } else { 52 | return iterateHash(result, key, count) 53 | } 54 | } 55 | 56 | function getEncodedChunk(hash: Uint8Array, offset: number): string { 57 | const chunk = 58 | (hash[offset] * Math.pow(2, 32) + 59 | hash[offset + 1] * Math.pow(2, 24) + 60 | hash[offset + 2] * Math.pow(2, 16) + 61 | hash[offset + 3] * Math.pow(2, 8) + 62 | hash[offset + 4]) % 63 | 100000 64 | let s = chunk.toString() 65 | while (s.length < 5) { 66 | s = '0' + s 67 | } 68 | return s 69 | } 70 | 71 | function shortToArrayBuffer(number) { 72 | return new Uint16Array([number]).buffer 73 | } 74 | 75 | function concatArrayBuffers(bufs: ArrayBuffer[]): ArrayBuffer { 76 | const lengths = bufs.map((b) => b.byteLength) 77 | const totalLength = lengths.reduce((p, c) => p + c, 0) 78 | const result = new Uint8Array(totalLength) 79 | lengths.reduce((p, c, i) => { 80 | result.set(new Uint8Array(bufs[i]), p) 81 | return p + c 82 | }, 0) 83 | 84 | return result.buffer 85 | } 86 | -------------------------------------------------------------------------------- /src/__test__/session-lock.test.ts: -------------------------------------------------------------------------------- 1 | import { SessionLock } from '../session-lock' 2 | 3 | async function sleep(ms: number) { 4 | return new Promise((resolve) => setTimeout(resolve, ms)) 5 | } 6 | describe('session-lock', function () { 7 | test('return something', async () => { 8 | let value = '' 9 | await Promise.all([ 10 | SessionLock.queueJobForNumber('channel1', async () => { 11 | await sleep(10) 12 | value += 'xyz' 13 | return Promise.resolve() 14 | }), 15 | ]) 16 | 17 | expect(value).toBe('xyz') 18 | }) 19 | 20 | test('return longshort', async () => { 21 | let value = '' 22 | await Promise.all([ 23 | SessionLock.queueJobForNumber('channel1', async () => { 24 | await sleep(3000) 25 | value += 'long' 26 | }), 27 | SessionLock.queueJobForNumber('channel1', async () => { 28 | await sleep(1) 29 | value += 'short' 30 | }), 31 | ]) 32 | 33 | expect(value).toBe('longshort') 34 | }) 35 | 36 | test('return shortlong', async () => { 37 | let value = '' 38 | await Promise.all([ 39 | SessionLock.queueJobForNumber('channel1', async () => { 40 | await sleep(1) 41 | value += 'short' 42 | }), 43 | SessionLock.queueJobForNumber('channel1', async () => { 44 | await sleep(2000) 45 | value += 'long' 46 | }), 47 | ]) 48 | 49 | expect(value).toBe('shortlong') 50 | }) 51 | 52 | test('multichannel', async () => { 53 | let value = '' 54 | await Promise.all([ 55 | SessionLock.queueJobForNumber('channel1', async () => { 56 | await sleep(4000) 57 | value += 'long' 58 | }), 59 | SessionLock.queueJobForNumber('channel2', async () => { 60 | await sleep(1) 61 | value += 'ch2' 62 | }), 63 | SessionLock.queueJobForNumber('channel1', async () => { 64 | await sleep(1) 65 | value += 'short' 66 | }), 67 | ]) 68 | const re = /ch2/gi 69 | const newstr = value.replace(re, '') 70 | expect(newstr).toBe('longshort') 71 | }) 72 | 73 | test('clear queue', async () => { 74 | let value = '' 75 | SessionLock.queueJobForNumber('channel1', async () => { 76 | await sleep(4000) 77 | value += 'long' 78 | }) 79 | SessionLock.queueJobForNumber('channel2', async () => { 80 | await sleep(1) 81 | value += 'ch2' 82 | }) 83 | SessionLock.queueJobForNumber('channel1', async () => { 84 | await sleep(1) 85 | value += 'short' 86 | }) 87 | 88 | await SessionLock.clearQueue() 89 | 90 | const re = /ch2/gi 91 | const newstr = value.replace(re, '') 92 | expect(newstr).toBe('longshort') 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /src/__test__/fingerprint-generator.test.ts: -------------------------------------------------------------------------------- 1 | import { FingerprintGenerator } from '../fingerprint-generator' 2 | import * as Internal from '../internal' 3 | 4 | describe('NumericFingerprint', function () { 5 | const ALICE_IDENTITY = [ 6 | 0x05, 0x06, 0x86, 0x3b, 0xc6, 0x6d, 0x02, 0xb4, 0x0d, 0x27, 0xb8, 0xd4, 0x9c, 0xa7, 0xc0, 0x9e, 0x92, 0x39, 7 | 0x23, 0x6f, 0x9d, 0x7d, 0x25, 0xd6, 0xfc, 0xca, 0x5c, 0xe1, 0x3c, 0x70, 0x64, 0xd8, 0x68, 8 | ] 9 | const BOB_IDENTITY = [ 10 | 0x05, 0xf7, 0x81, 0xb6, 0xfb, 0x32, 0xfe, 0xd9, 0xba, 0x1c, 0xf2, 0xde, 0x97, 0x8d, 0x4d, 0x5d, 0xa2, 0x8d, 11 | 0xc3, 0x40, 0x46, 0xae, 0x81, 0x44, 0x02, 0xb5, 0xc0, 0xdb, 0xd9, 0x6f, 0xda, 0x90, 0x7b, 12 | ] 13 | const FINGERPRINT = '300354477692869396892869876765458257569162576843440918079131' 14 | 15 | const alice = { 16 | identifier: '+14152222222', 17 | key: new Uint8Array(ALICE_IDENTITY).buffer, 18 | } 19 | const bob = { 20 | identifier: '+14153333333', 21 | key: new Uint8Array(BOB_IDENTITY).buffer, 22 | } 23 | 24 | test('returns the correct fingerprint', async () => { 25 | jest.setTimeout(20000) 26 | const generator = new FingerprintGenerator(5200) 27 | const t = Date.now() 28 | const f = await generator.createFor(alice.identifier, alice.key, bob.identifier, bob.key) 29 | console.log(`import crypto time:`, { time: Date.now() - t }) 30 | expect(f).toBe(FINGERPRINT) 31 | }) 32 | 33 | test('alice and bob results match', async () => { 34 | jest.setTimeout(10000) 35 | const generator = new FingerprintGenerator(1024) 36 | const a = await generator.createFor(alice.identifier, alice.key, bob.identifier, bob.key) 37 | const b = await generator.createFor(bob.identifier, bob.key, alice.identifier, alice.key) 38 | expect(a).toBe(b) 39 | }) 40 | 41 | test('alice and !bob results mismatch', async () => { 42 | jest.setTimeout(10000) 43 | const generator = new FingerprintGenerator(1024) 44 | const a = await generator.createFor(alice.identifier, alice.key, '+15558675309', bob.key) 45 | const b = await generator.createFor(bob.identifier, bob.key, alice.identifier, alice.key) 46 | expect(a).not.toBe(b) 47 | }) 48 | 49 | test('alice and mitm results mismatch', async () => { 50 | jest.setTimeout(10000) 51 | const mitm = Internal.crypto.getRandomBytes(33) 52 | const generator = new FingerprintGenerator(1024) 53 | const a = await generator.createFor(alice.identifier, alice.key, bob.identifier, mitm) 54 | const b = await generator.createFor(bob.identifier, bob.key, alice.identifier, alice.key) 55 | expect(a).not.toBe(b) 56 | }) 57 | 58 | test('inject alternate crypto', async () => { 59 | jest.setTimeout(20000) 60 | const oldcrypto = Internal.crypto.webcrypto 61 | // eslint-disable-next-line @typescript-eslint/no-var-requires 62 | const newcrypto = require('../../lib/msrcrypto') 63 | Internal.setWebCrypto(newcrypto) 64 | const t = Date.now() 65 | const generator = new FingerprintGenerator(5200) 66 | const f = await generator.createFor(alice.identifier, alice.key, bob.identifier, bob.key) 67 | console.log(`injected crypto time:`, { time: Date.now() - t }) 68 | expect(f).toBe(FINGERPRINT) 69 | Internal.setWebCrypto(oldcrypto) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | export interface SignalProtocolAddressType { 4 | readonly name: string 5 | readonly deviceId: number 6 | toString: () => string 7 | equals: (other: SignalProtocolAddressType) => boolean 8 | } 9 | 10 | export interface FingerprintGeneratorType { 11 | createFor: ( 12 | localIdentifier: string, 13 | localIdentityKey: ArrayBuffer, 14 | remoteIdentifier: string, 15 | remoteIdentityKey: ArrayBuffer 16 | ) => Promise 17 | } 18 | 19 | export interface KeyPairType { 20 | pubKey: T 21 | privKey: T 22 | } 23 | 24 | export interface PreKeyPairType { 25 | keyId: number 26 | keyPair: KeyPairType 27 | } 28 | 29 | export interface SignedPreKeyPairType extends PreKeyPairType { 30 | signature: T 31 | } 32 | 33 | export interface PreKeyType { 34 | keyId: number 35 | publicKey: T 36 | } 37 | 38 | export interface SignedPublicPreKeyType extends PreKeyType { 39 | signature: T 40 | } 41 | 42 | export type SessionRecordType = string 43 | 44 | export enum Direction { 45 | SENDING = 1, 46 | RECEIVING = 2, 47 | } 48 | export interface StorageType { 49 | getIdentityKeyPair: () => Promise 50 | getLocalRegistrationId: () => Promise 51 | 52 | isTrustedIdentity: (identifier: string, identityKey: ArrayBuffer, direction: Direction) => Promise 53 | saveIdentity: (encodedAddress: string, publicKey: ArrayBuffer, nonblockingApproval?: boolean) => Promise 54 | 55 | loadPreKey: (encodedAddress: string | number) => Promise 56 | storePreKey: (keyId: number | string, keyPair: KeyPairType) => Promise 57 | removePreKey: (keyId: number | string) => Promise 58 | 59 | storeSession: (encodedAddress: string, record: SessionRecordType) => Promise 60 | loadSession: (encodedAddress: string) => Promise 61 | 62 | // This returns a KeyPairType, but note that it's the implenenter's responsibility to validate! 63 | loadSignedPreKey: (keyId: number | string) => Promise 64 | storeSignedPreKey: (keyId: number | string, keyPair: KeyPairType) => Promise 65 | removeSignedPreKey: (keyId: number | string) => Promise 66 | } 67 | 68 | export interface CurveType { 69 | generateKeyPair: () => Promise 70 | createKeyPair: (privKey: ArrayBuffer) => Promise 71 | calculateAgreement: (pubKey: ArrayBuffer, privKey: ArrayBuffer) => Promise 72 | verifySignature: (pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer) => Promise 73 | calculateSignature: (privKey: ArrayBuffer, message: ArrayBuffer) => ArrayBuffer | Promise 74 | validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer 75 | } 76 | 77 | export interface AsyncCurveType { 78 | generateKeyPair: () => Promise 79 | createKeyPair: (privKey: ArrayBuffer) => Promise 80 | calculateAgreement: (pubKey: ArrayBuffer, privKey: ArrayBuffer) => Promise 81 | verifySignature: (pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer) => Promise 82 | calculateSignature: (privKey: ArrayBuffer, message: ArrayBuffer) => Promise 83 | } 84 | -------------------------------------------------------------------------------- /src/internal/curve.ts: -------------------------------------------------------------------------------- 1 | import { KeyPairType } from '../types' 2 | import { 3 | Curve25519Wrapper, 4 | AsyncCurve25519Wrapper, 5 | AsyncCurve as AsyncCurveType, 6 | Curve as CurveType, 7 | } from '@privacyresearch/curve25519-typescript' 8 | import { uint8ArrayToArrayBuffer } from '../helpers' 9 | 10 | export class Curve { 11 | // Curve 25519 crypto 12 | private _curve25519: CurveType 13 | async: AsyncCurve 14 | constructor(curve25519: Curve25519Wrapper) { 15 | this._curve25519 = curve25519 16 | this.async = new AsyncCurve() 17 | } 18 | 19 | set curve(c: CurveType) { 20 | this._curve25519 = c 21 | } 22 | 23 | createKeyPair(privKey: ArrayBuffer): KeyPairType { 24 | validatePrivKey(privKey) 25 | const raw_keys = this._curve25519.keyPair(privKey) 26 | return processKeys(raw_keys) 27 | } 28 | 29 | ECDHE(pubKey: ArrayBuffer, privKey: ArrayBuffer): ArrayBuffer { 30 | pubKey = validatePubKeyFormat(pubKey) 31 | validatePrivKey(privKey) 32 | 33 | if (pubKey === undefined || pubKey.byteLength != 32) { 34 | throw new Error('Invalid public key') 35 | } 36 | 37 | return this._curve25519.sharedSecret(pubKey, privKey) 38 | } 39 | 40 | Ed25519Sign(privKey: ArrayBuffer, message: ArrayBuffer): ArrayBuffer { 41 | validatePrivKey(privKey) 42 | 43 | if (message === undefined) { 44 | throw new Error('Invalid message') 45 | } 46 | 47 | return this._curve25519.sign(privKey, message) 48 | } 49 | 50 | Ed25519Verify(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): boolean { 51 | pubKey = validatePubKeyFormat(pubKey) 52 | 53 | if (pubKey === undefined || pubKey.byteLength != 32) { 54 | throw new Error('Invalid public key') 55 | } 56 | 57 | if (msg === undefined) { 58 | throw new Error('Invalid message') 59 | } 60 | 61 | if (sig === undefined || sig.byteLength != 64) { 62 | throw new Error('Invalid signature') 63 | } 64 | 65 | return this._curve25519.verify(pubKey, msg, sig) 66 | } 67 | } 68 | 69 | export class AsyncCurve { 70 | private _curve25519: AsyncCurveType 71 | constructor() { 72 | this._curve25519 = new AsyncCurve25519Wrapper() 73 | } 74 | 75 | set curve(c: AsyncCurveType) { 76 | this._curve25519 = c 77 | } 78 | 79 | async createKeyPair(privKey: ArrayBuffer): Promise { 80 | validatePrivKey(privKey) 81 | const raw_keys = await this._curve25519.keyPair(privKey) 82 | return processKeys(raw_keys) 83 | } 84 | 85 | ECDHE(pubKey: ArrayBuffer, privKey: ArrayBuffer): Promise { 86 | pubKey = validatePubKeyFormat(pubKey) 87 | validatePrivKey(privKey) 88 | 89 | if (pubKey === undefined || pubKey.byteLength != 32) { 90 | throw new Error('Invalid public key') 91 | } 92 | 93 | return this._curve25519.sharedSecret(pubKey, privKey) 94 | } 95 | 96 | Ed25519Sign(privKey: ArrayBuffer, message: ArrayBuffer): Promise { 97 | validatePrivKey(privKey) 98 | 99 | if (message === undefined) { 100 | throw new Error('Invalid message') 101 | } 102 | 103 | return this._curve25519.sign(privKey, message) 104 | } 105 | 106 | async Ed25519Verify(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): Promise { 107 | pubKey = validatePubKeyFormat(pubKey) 108 | 109 | if (pubKey === undefined || pubKey.byteLength != 32) { 110 | throw new Error('Invalid public key') 111 | } 112 | 113 | if (msg === undefined) { 114 | throw new Error('Invalid message') 115 | } 116 | 117 | if (sig === undefined || sig.byteLength != 64) { 118 | throw new Error('Invalid signature') 119 | } 120 | 121 | const verifyResult = await this._curve25519.verify(pubKey, msg, sig) 122 | 123 | if (verifyResult) { 124 | throw new Error('Invalid signature') 125 | } 126 | 127 | return verifyResult 128 | } 129 | } 130 | 131 | function validatePrivKey(privKey: unknown): void { 132 | if (privKey === undefined || !(privKey instanceof ArrayBuffer) || privKey.byteLength != 32) { 133 | throw new Error('Invalid private key') 134 | } 135 | } 136 | function validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer { 137 | if ( 138 | pubKey === undefined || 139 | ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32) 140 | ) { 141 | console.warn(`Invalid public key`, { pubKey }) 142 | throw new Error(`Invalid public key: ${pubKey} ${pubKey?.byteLength}`) 143 | } 144 | if (pubKey.byteLength == 33) { 145 | return pubKey.slice(1) 146 | } else { 147 | console.error( 148 | 'WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey' 149 | ) 150 | return pubKey 151 | } 152 | } 153 | 154 | function processKeys(raw_keys: KeyPairType): KeyPairType { 155 | // prepend version byte 156 | const origPub = new Uint8Array(raw_keys.pubKey) 157 | const pub = new Uint8Array(33) 158 | pub.set(origPub, 1) 159 | pub[0] = 5 160 | 161 | return { pubKey: uint8ArrayToArrayBuffer(pub), privKey: raw_keys.privKey } 162 | } 163 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | "allowJs": true /* Allow javascript files to be compiled. */, 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | "declaration": true /* Generates corresponding '.d.ts' file. */, 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./lib/index.js", /* Concatenate and emit output to single file. */ 16 | "outDir": "./lib" /* Redirect output structure to the directory. */, 17 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | "rootDirs": [ 47 | "src/" 48 | ] /* List of root folders whose combined content represents the structure of the project at runtime. */, 49 | // "typeRoots": ["./types", "./node_modules/@types/"] /* List of folders to include type definitions from. */, 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 68 | "skipLibCheck": true 69 | }, 70 | "include": ["src/**/*"] 71 | } 72 | -------------------------------------------------------------------------------- /src/internal/crypto.ts: -------------------------------------------------------------------------------- 1 | import * as Internal from '.' 2 | import * as util from '../helpers' 3 | import { KeyPairType } from '../types' 4 | import { AsyncCurve as AsyncCurveType } from '@privacyresearch/curve25519-typescript' 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const webcrypto = globalThis?.crypto || require('../../lib/msrcrypto') // globalThis?.crypto || window?.crypto || require('../../lib/msrcrypto') 8 | 9 | export class Crypto { 10 | private _curve: Internal.AsyncCurve 11 | private _webcrypto: globalThis.Crypto 12 | 13 | constructor(crypto?: globalThis.Crypto) { 14 | this._curve = new Internal.AsyncCurve() 15 | this._webcrypto = crypto || webcrypto 16 | } 17 | 18 | set webcrypto(wc: globalThis.Crypto) { 19 | this._webcrypto = wc 20 | } 21 | set curve(c: AsyncCurveType) { 22 | this._curve.curve = c 23 | } 24 | 25 | getRandomBytes(n: number): ArrayBuffer { 26 | const array = new Uint8Array(n) 27 | this._webcrypto.getRandomValues(array) 28 | return util.uint8ArrayToArrayBuffer(array) 29 | } 30 | 31 | async encrypt(key: ArrayBuffer, data: ArrayBuffer, iv: ArrayBuffer): Promise { 32 | const impkey = await this._webcrypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']) 33 | 34 | return this._webcrypto.subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, impkey, data) 35 | } 36 | 37 | async decrypt(key: ArrayBuffer, data: ArrayBuffer, iv: ArrayBuffer): Promise { 38 | const impkey = await this._webcrypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt']) 39 | 40 | return this._webcrypto.subtle.decrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, impkey, data) 41 | } 42 | async sign(key: ArrayBuffer, data: ArrayBuffer): Promise { 43 | const impkey = await this._webcrypto.subtle.importKey( 44 | 'raw', 45 | key, 46 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 47 | false, 48 | ['sign'] 49 | ) 50 | 51 | try { 52 | return this._webcrypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, impkey, data) 53 | } catch (e) { 54 | // console.log({ e, data, impkey }) 55 | throw e 56 | } 57 | } 58 | async hash(data: ArrayBuffer): Promise { 59 | return this._webcrypto.subtle.digest({ name: 'SHA-512' }, data) 60 | } 61 | 62 | async HKDF(input: ArrayBuffer, salt: ArrayBuffer, info: ArrayBuffer): Promise { 63 | // Specific implementation of RFC 5869 that only returns the first 3 32-byte chunks 64 | if (typeof info === 'string') { 65 | throw new Error(`HKDF info was a string`) 66 | } 67 | const PRK = await Internal.crypto.sign(salt, input) 68 | const infoBuffer = new ArrayBuffer(info.byteLength + 1 + 32) 69 | const infoArray = new Uint8Array(infoBuffer) 70 | infoArray.set(new Uint8Array(info), 32) 71 | infoArray[infoArray.length - 1] = 1 72 | const T1 = await Internal.crypto.sign(PRK, infoBuffer.slice(32)) 73 | infoArray.set(new Uint8Array(T1)) 74 | infoArray[infoArray.length - 1] = 2 75 | const T2 = await Internal.crypto.sign(PRK, infoBuffer) 76 | infoArray.set(new Uint8Array(T2)) 77 | infoArray[infoArray.length - 1] = 3 78 | const T3 = await Internal.crypto.sign(PRK, infoBuffer) 79 | return [T1, T2, T3] 80 | } 81 | 82 | // Curve25519 crypto 83 | 84 | createKeyPair(privKey?: ArrayBuffer): Promise { 85 | if (!privKey) { 86 | privKey = this.getRandomBytes(32) 87 | } 88 | return this._curve.createKeyPair(privKey) 89 | } 90 | 91 | ECDHE(pubKey: ArrayBuffer, privKey: ArrayBuffer): Promise { 92 | return this._curve.ECDHE(pubKey, privKey) 93 | } 94 | 95 | Ed25519Sign(privKey: ArrayBuffer, message: ArrayBuffer): Promise { 96 | return this._curve.Ed25519Sign(privKey, message) 97 | } 98 | 99 | Ed25519Verify(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): Promise { 100 | return this._curve.Ed25519Verify(pubKey, msg, sig) 101 | } 102 | } 103 | 104 | export const crypto = new Crypto() 105 | 106 | export function setWebCrypto(webcrypto: globalThis.Crypto): void { 107 | crypto.webcrypto = webcrypto 108 | } 109 | 110 | export function setCurve(curve: AsyncCurveType): void { 111 | crypto.curve = curve 112 | } 113 | 114 | // HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes 115 | export function HKDF(input: ArrayBuffer, salt: ArrayBuffer, info: string): Promise { 116 | if (salt.byteLength != 32) { 117 | throw new Error('Got salt of incorrect length') 118 | } 119 | 120 | const abInfo = util.binaryStringToArrayBuffer(info) 121 | if (!abInfo) { 122 | throw new Error(`Invalid HKDF info`) 123 | } 124 | 125 | return crypto.HKDF(input, salt, abInfo) 126 | } 127 | 128 | export async function verifyMAC(data: ArrayBuffer, key: ArrayBuffer, mac: ArrayBuffer, length: number): Promise { 129 | const calculated_mac = await crypto.sign(key, data) 130 | if (mac.byteLength != length || calculated_mac.byteLength < length) { 131 | throw new Error('Bad MAC length') 132 | } 133 | const a = new Uint8Array(calculated_mac) 134 | const b = new Uint8Array(mac) 135 | let result = 0 136 | for (let i = 0; i < mac.byteLength; ++i) { 137 | result = result | (a[i] ^ b[i]) 138 | } 139 | if (result !== 0) { 140 | throw new Error('Bad MAC') 141 | } 142 | } 143 | 144 | export function calculateMAC(key: ArrayBuffer, data: ArrayBuffer): Promise { 145 | return crypto.sign(key, data) 146 | } 147 | -------------------------------------------------------------------------------- /src/__test__/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { hexToArrayBuffer, assertEqualArrayBuffers } from '../__test-utils__/utils' 2 | import * as Internal from '../internal' 3 | import { uint8ArrayToArrayBuffer } from '../helpers' 4 | 5 | describe('New Crypto Tests 2020', function () { 6 | const alice_bytes = hexToArrayBuffer('77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a') 7 | const alice_priv = hexToArrayBuffer('70076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c6a') 8 | const alice_pub = hexToArrayBuffer('058520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a') 9 | const bob_bytes = hexToArrayBuffer('5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb') 10 | const bob_priv = hexToArrayBuffer('58ab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e06b') 11 | const bob_pub = hexToArrayBuffer('05de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f') 12 | const shared_sec = hexToArrayBuffer('4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742') 13 | 14 | test(`createKeyPair converts alice's private keys to a keypair`, async () => { 15 | const alicekeypair = await Internal.crypto.createKeyPair(alice_bytes) 16 | assertEqualArrayBuffers(alicekeypair.privKey, alice_priv) 17 | assertEqualArrayBuffers(alicekeypair.pubKey, alice_pub) 18 | 19 | const bobkeypair = await Internal.crypto.createKeyPair(bob_bytes) 20 | assertEqualArrayBuffers(bobkeypair.privKey, bob_priv) 21 | assertEqualArrayBuffers(bobkeypair.pubKey, bob_pub) 22 | }) 23 | 24 | test(`createKeyPair generates a key if not provided`, async () => { 25 | const keypair = await Internal.crypto.createKeyPair() 26 | expect(keypair.privKey.byteLength).toStrictEqual(32) 27 | expect(keypair.pubKey.byteLength).toStrictEqual(33) 28 | expect(new Uint8Array(keypair.pubKey)[0]).toStrictEqual(5) 29 | }) 30 | 31 | test(`ECDHE computes the shared secret for alice`, async () => { 32 | const secret = await Internal.crypto.ECDHE(bob_pub, alice_priv) 33 | assertEqualArrayBuffers(shared_sec, secret) 34 | }) 35 | 36 | test(`ECDHE computes the shared secret for bob`, async () => { 37 | const secret = await Internal.crypto.ECDHE(alice_pub, bob_priv) 38 | assertEqualArrayBuffers(shared_sec, secret) 39 | }) 40 | const priv = hexToArrayBuffer('48a8892cc4e49124b7b57d94fa15becfce071830d6449004685e387c62409973') 41 | const pub = hexToArrayBuffer('0555f1bfede27b6a03e0dd389478ffb01462e5c52dbbac32cf870f00af1ed9af3a') 42 | const msg = hexToArrayBuffer('617364666173646661736466') 43 | const sig = hexToArrayBuffer( 44 | '2bc06c745acb8bae10fbc607ee306084d0c28e2b3bb819133392473431291fd0dfa9c7f11479996cf520730d2901267387e08d85bbf2af941590e3035a545285' 45 | ) 46 | 47 | test(`Ed25519Sign works`, async () => { 48 | const sigCalc = await Internal.crypto.Ed25519Sign(priv, msg) 49 | assertEqualArrayBuffers(sig, sigCalc) 50 | }) 51 | 52 | test(`Ed25519Verify throws on bad signature`, async () => { 53 | const badsig = sig.slice(0) 54 | new Uint8Array(badsig).set([0], 0) 55 | 56 | try { 57 | await Internal.crypto.Ed25519Verify(pub, msg, badsig) 58 | } catch (e) { 59 | if ((e as Error).message === 'Invalid signature') { 60 | return 61 | } 62 | } 63 | console.error('Sign did not throw on bad input') 64 | }) 65 | 66 | test(`Ed25519Verify does not throw on good signature`, async () => { 67 | const result = await Internal.crypto.Ed25519Verify(pub, msg, sig) 68 | 69 | // These functions return false on valid signature! The async ones 70 | // throw an error on invalid signature. The synchronous ones return 71 | // true on invalid signature. 72 | expect(result).toBe(false) 73 | }) 74 | }) 75 | 76 | describe('Crypto', function () { 77 | describe('Encrypt AES-CBC', function () { 78 | test('works', async () => { 79 | const key = hexToArrayBuffer('603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4') 80 | const iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f') 81 | const plaintext = hexToArrayBuffer( 82 | '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710' 83 | ) 84 | const ciphertext = hexToArrayBuffer( 85 | 'f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644' 86 | ) 87 | const result = await Internal.crypto.encrypt(key, plaintext, iv) 88 | assertEqualArrayBuffers(result, ciphertext) 89 | }) 90 | }) 91 | 92 | describe('Decrypt AES-CBC', function () { 93 | test('works', async () => { 94 | const key = hexToArrayBuffer('603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4') 95 | const iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f') 96 | const plaintext = hexToArrayBuffer( 97 | '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710' 98 | ) 99 | const ciphertext = hexToArrayBuffer( 100 | 'f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644' 101 | ) 102 | const result = await Internal.crypto.decrypt(key, ciphertext, iv) 103 | assertEqualArrayBuffers(result, plaintext) 104 | }) 105 | }) 106 | 107 | describe('HMAC SHA-256', function () { 108 | test('works', async () => { 109 | const key = hexToArrayBuffer( 110 | '6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245' 111 | ) 112 | const input = hexToArrayBuffer( 113 | '752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b' 114 | ) 115 | const mac = hexToArrayBuffer('05d1243e6465ed9620c9aec1c351a186') 116 | const result = await Internal.crypto.sign(key, input) 117 | assertEqualArrayBuffers(result.slice(0, mac.byteLength), mac) 118 | }) 119 | }) 120 | 121 | describe('HKDF', function () { 122 | test('works', async () => { 123 | // HMAC RFC5869 Test vectors 124 | const T1 = hexToArrayBuffer('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf') 125 | const T2 = hexToArrayBuffer('34007208d5b887185865') 126 | const IKM = new Uint8Array(new ArrayBuffer(22)) 127 | for (let i = 0; i < 22; i++) IKM[i] = 11 128 | 129 | const salt = new Uint8Array(new ArrayBuffer(13)) 130 | for (let i = 0; i < 13; i++) salt[i] = i 131 | 132 | const info = new Uint8Array(new ArrayBuffer(10)) 133 | for (let i = 0; i < 10; i++) info[i] = 240 + i 134 | 135 | const OKM = await Internal.crypto.HKDF( 136 | uint8ArrayToArrayBuffer(IKM), 137 | uint8ArrayToArrayBuffer(salt), 138 | uint8ArrayToArrayBuffer(info) 139 | ) 140 | assertEqualArrayBuffers(OKM[0], T1) 141 | assertEqualArrayBuffers(OKM[1].slice(0, 10), T2) 142 | }) 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /src/__test__/storage-type.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { SignalProtocolAddress } from '../signal-protocol-address' 4 | import { StorageType, Direction, SessionRecordType, PreKeyPairType, SignedPreKeyPairType } from '../types' 5 | import * as util from '../helpers' 6 | 7 | // Type guards 8 | export function isKeyPairType(kp: any): kp is KeyPairType { 9 | return !!(kp?.privKey && kp?.pubKey) 10 | } 11 | 12 | export function isPreKeyType(pk: any): pk is PreKeyPairType { 13 | return typeof pk?.keyId === 'number' && isKeyPairType(pk?.keyPair) 14 | } 15 | 16 | export function isSignedPreKeyType(spk: any): spk is SignedPreKeyPairType { 17 | return spk?.signature && isPreKeyType(spk) 18 | } 19 | 20 | interface KeyPairType { 21 | pubKey: ArrayBuffer 22 | privKey: ArrayBuffer 23 | } 24 | 25 | interface PreKeyType { 26 | keyId: number 27 | keyPair: KeyPairType 28 | } 29 | interface SignedPreKeyType extends PreKeyType { 30 | signature: ArrayBuffer 31 | } 32 | 33 | function isArrayBuffer(thing: StoreValue): boolean { 34 | const t = typeof thing 35 | return !!thing && t !== 'string' && t !== 'number' && 'byteLength' in (thing as any) 36 | } 37 | 38 | type StoreValue = KeyPairType | string | number | KeyPairType | PreKeyType | SignedPreKeyType | ArrayBuffer | undefined 39 | 40 | export class SignalProtocolStore implements StorageType { 41 | private _store: Record 42 | 43 | constructor() { 44 | this._store = {} 45 | } 46 | // 47 | get(key: string, defaultValue: StoreValue): StoreValue { 48 | if (key === null || key === undefined) throw new Error('Tried to get value for undefined/null key') 49 | if (key in this._store) { 50 | return this._store[key] 51 | } else { 52 | return defaultValue 53 | } 54 | } 55 | remove(key: string): void { 56 | if (key === null || key === undefined) throw new Error('Tried to remove value for undefined/null key') 57 | delete this._store[key] 58 | } 59 | put(key: string, value: StoreValue): void { 60 | if (key === undefined || value === undefined || key === null || value === null) 61 | throw new Error('Tried to store undefined/null') 62 | this._store[key] = value 63 | } 64 | 65 | async getIdentityKeyPair(): Promise { 66 | const kp = this.get('identityKey', undefined) 67 | if (isKeyPairType(kp) || typeof kp === 'undefined') { 68 | return kp 69 | } 70 | throw new Error('Item stored as identity key of unknown type.') 71 | } 72 | 73 | async getLocalRegistrationId(): Promise { 74 | const rid = this.get('registrationId', undefined) 75 | if (typeof rid === 'number' || typeof rid === 'undefined') { 76 | return rid 77 | } 78 | throw new Error('Stored Registration ID is not a number') 79 | } 80 | isTrustedIdentity( 81 | identifier: string, 82 | identityKey: ArrayBuffer, 83 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 84 | _direction: Direction 85 | ): Promise { 86 | if (identifier === null || identifier === undefined) { 87 | throw new Error('tried to check identity key for undefined/null key') 88 | } 89 | const trusted = this.get('identityKey' + identifier, undefined) 90 | 91 | // TODO: Is this right? If the ID is NOT in our store we trust it? 92 | if (trusted === undefined) { 93 | return Promise.resolve(true) 94 | } 95 | return Promise.resolve( 96 | util.arrayBufferToString(identityKey) === util.arrayBufferToString(trusted as ArrayBuffer) 97 | ) 98 | } 99 | async loadPreKey(keyId: string | number): Promise { 100 | let res = this.get('25519KeypreKey' + keyId, undefined) 101 | if (isKeyPairType(res)) { 102 | res = { pubKey: res.pubKey, privKey: res.privKey } 103 | return res 104 | } else if (typeof res === 'undefined') { 105 | return res 106 | } 107 | throw new Error(`stored key has wrong type`) 108 | } 109 | async loadSession(identifier: string): Promise { 110 | const rec = this.get('session' + identifier, undefined) 111 | if (typeof rec === 'string') { 112 | return rec as string 113 | } else if (typeof rec === 'undefined') { 114 | return rec 115 | } 116 | throw new Error(`session record is not an ArrayBuffer`) 117 | } 118 | 119 | async loadSignedPreKey(keyId: number | string): Promise { 120 | const res = this.get('25519KeysignedKey' + keyId, undefined) 121 | if (isKeyPairType(res)) { 122 | return { pubKey: res.pubKey, privKey: res.privKey } 123 | } else if (typeof res === 'undefined') { 124 | return res 125 | } 126 | throw new Error(`stored key has wrong type`) 127 | } 128 | async removePreKey(keyId: number | string): Promise { 129 | this.remove('25519KeypreKey' + keyId) 130 | } 131 | async saveIdentity(identifier: string, identityKey: ArrayBuffer): Promise { 132 | if (identifier === null || identifier === undefined) 133 | throw new Error('Tried to put identity key for undefined/null key') 134 | 135 | const address = SignalProtocolAddress.fromString(identifier) 136 | 137 | const existing = this.get('identityKey' + address.getName(), undefined) 138 | this.put('identityKey' + address.getName(), identityKey) 139 | if (existing && !isArrayBuffer(existing)) { 140 | throw new Error('Identity Key is incorrect type') 141 | } 142 | 143 | if (existing && util.arrayBufferToString(identityKey) !== util.arrayBufferToString(existing as ArrayBuffer)) { 144 | return true 145 | } else { 146 | return false 147 | } 148 | } 149 | async storeSession(identifier: string, record: SessionRecordType): Promise { 150 | return this.put('session' + identifier, record) 151 | } 152 | async loadIdentityKey(identifier: string): Promise { 153 | if (identifier === null || identifier === undefined) { 154 | throw new Error('Tried to get identity key for undefined/null key') 155 | } 156 | 157 | const key = this.get('identityKey' + identifier, undefined) 158 | if (isArrayBuffer(key)) { 159 | return key as ArrayBuffer 160 | } else if (typeof key === 'undefined') { 161 | return key 162 | } 163 | throw new Error(`Identity key has wrong type`) 164 | } 165 | async storePreKey(keyId: number | string, keyPair: KeyPairType): Promise { 166 | return this.put('25519KeypreKey' + keyId, keyPair) 167 | } 168 | 169 | // TODO: Why is this keyId a number where others are strings? 170 | async storeSignedPreKey(keyId: number | string, keyPair: KeyPairType): Promise { 171 | return this.put('25519KeysignedKey' + keyId, keyPair) 172 | } 173 | async removeSignedPreKey(keyId: number | string): Promise { 174 | return this.remove('25519KeysignedKey' + keyId) 175 | } 176 | async removeSession(identifier: string): Promise { 177 | return this.remove('session' + identifier) 178 | } 179 | async removeAllSessions(identifier: string): Promise { 180 | for (const id in this._store) { 181 | if (id.startsWith('session' + identifier)) { 182 | delete this._store[id] 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/__test__/session-builder.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import { SessionBuilder } from '../session-builder' 3 | import { SessionCipher } from '../session-cipher' 4 | import { SessionRecord } from '../session-record' 5 | 6 | import { SignalProtocolAddress } from '../signal-protocol-address' 7 | import { SignalProtocolStore } from './storage-type' 8 | 9 | import { generateIdentity, generatePreKeyBundle, assertEqualArrayBuffers } from '../__test-utils__/utils' 10 | import * as utils from '../helpers' 11 | import { KeyHelper } from '../key-helper' 12 | 13 | jest.setTimeout(30000) 14 | 15 | const ALICE_ADDRESS = new SignalProtocolAddress('+14151111111', 1) 16 | const BOB_ADDRESS = new SignalProtocolAddress('+14152222222', 1) 17 | 18 | describe('basic prekey v3', function () { 19 | const aliceStore = new SignalProtocolStore() 20 | const bobStore = new SignalProtocolStore() 21 | const bobPreKeyId = 1337 22 | const bobSignedKeyId = 1 23 | 24 | beforeAll(async () => { 25 | await Promise.all([generateIdentity(aliceStore), generateIdentity(bobStore)]) 26 | const preKeyBundle = await generatePreKeyBundle(bobStore, bobPreKeyId, bobSignedKeyId) 27 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 28 | return builder.processPreKey(preKeyBundle) 29 | }) 30 | 31 | const originalMessage = utils.binaryStringToArrayBuffer("L'homme est condamné à être libre") 32 | const nextMessage = ( 33 | utils.binaryStringToArrayBuffer( 34 | "condamné parce qu'il ne s'est pas crée lui-même, et par ailleurs cependant libre parce qu'une fois jeté dans le monde, il est responsable de tout ce qu'il fait." 35 | ) 36 | ) 37 | const aliceSessionCipher = new SessionCipher(aliceStore, BOB_ADDRESS) 38 | const bobSessionCipher = new SessionCipher(bobStore, ALICE_ADDRESS) 39 | 40 | test('basic prekey v3: creates a session', async () => { 41 | const record = await aliceStore.loadSession(BOB_ADDRESS.toString()) 42 | expect(record).toBeDefined() 43 | const sessionRecord = SessionRecord.deserialize(record!) 44 | expect(sessionRecord.haveOpenSession()).toBeTruthy() 45 | expect(sessionRecord.getOpenSession()).toBeDefined() 46 | }) 47 | 48 | test('basic prekey v3: the session can encrypt', async () => { 49 | const ciphertext = await aliceSessionCipher.encrypt(originalMessage) 50 | expect(ciphertext.type).toBe(3) // PREKEY_BUNDLE 51 | const plaintext = await bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext.body!, 'binary') 52 | assertEqualArrayBuffers(plaintext, originalMessage) // assertEqualArrayBuffers(plaintext, originalMessage) 53 | }) 54 | 55 | test('basic v3 NO PREKEY: the session can decrypt multiple v3 messages', async () => { 56 | const ciphertext0 = await aliceSessionCipher.encrypt(originalMessage) 57 | expect(ciphertext0.type).toBe(3) // PREKEY_BUNDLE 58 | const ciphertext1 = await aliceSessionCipher.encrypt(nextMessage) 59 | expect(ciphertext1.type).toBe(3) // PREKEY_BUNDLE 60 | const plaintext0 = await bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext0.body!, 'binary') 61 | assertEqualArrayBuffers(plaintext0, originalMessage) 62 | const plaintext1 = await bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext1.body!, 'binary') 63 | assertEqualArrayBuffers(plaintext1, nextMessage) 64 | }) 65 | 66 | test('basic prekey v3: the session can decrypt', async () => { 67 | const ciphertext = await bobSessionCipher.encrypt(originalMessage) 68 | const plaintext = await aliceSessionCipher.decryptWhisperMessage(ciphertext.body!, 'binary') 69 | assertEqualArrayBuffers(plaintext, originalMessage) 70 | }) 71 | 72 | test('basic prekey v3: accepts a new preKey with the same identity', async () => { 73 | const preKeyBundle = await generatePreKeyBundle(bobStore, bobPreKeyId + 1, bobSignedKeyId + 1) 74 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 75 | await builder.processPreKey(preKeyBundle) 76 | const record = await aliceStore.loadSession(BOB_ADDRESS.toString()) 77 | expect(record).toBeDefined() 78 | const sessionRecord = SessionRecord.deserialize(record!) 79 | expect(sessionRecord.haveOpenSession()).toBeTruthy() 80 | expect(sessionRecord.getOpenSession()).toBeDefined() 81 | }) 82 | 83 | test('basic prekey v3: rejects untrusted identity keys', async () => { 84 | const newIdentity = await KeyHelper.generateIdentityKeyPair() 85 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 86 | await expect(async () => { 87 | await builder.processPreKey({ 88 | identityKey: newIdentity.pubKey, 89 | registrationId: 12356, 90 | signedPreKey: { 91 | keyId: 2, 92 | publicKey: new Uint8Array(33).buffer, 93 | signature: new Uint8Array(32).buffer, 94 | }, 95 | }) 96 | }).rejects.toThrow('Identity key changed') 97 | }) 98 | }) 99 | 100 | describe('basic v3 NO PREKEY', function () { 101 | const aliceStore = new SignalProtocolStore() 102 | 103 | const bobStore = new SignalProtocolStore() 104 | const bobPreKeyId = 1337 105 | const bobSignedKeyId = 1 106 | 107 | beforeAll(async () => { 108 | await Promise.all([generateIdentity(aliceStore), generateIdentity(bobStore)]) 109 | const preKeyBundle = await generatePreKeyBundle(bobStore, bobPreKeyId, bobSignedKeyId) 110 | delete preKeyBundle.preKey 111 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 112 | return builder.processPreKey(preKeyBundle) 113 | }) 114 | 115 | const originalMessage = utils.binaryStringToArrayBuffer("L'homme est condamné à être libre") 116 | const nextMessage = ( 117 | utils.binaryStringToArrayBuffer( 118 | "condamné parce qu'il ne s'est pas crée lui-même, et par ailleurs cependant libre parce qu'une fois jeté dans le monde, il est responsable de tout ce qu'il fait." 119 | ) 120 | ) 121 | const aliceSessionCipher = new SessionCipher(aliceStore, BOB_ADDRESS) 122 | const bobSessionCipher = new SessionCipher(bobStore, ALICE_ADDRESS) 123 | 124 | test('basic v3 NO PREKEY: creates a session', async () => { 125 | const record = await aliceStore.loadSession(BOB_ADDRESS.toString()) 126 | expect(record).toBeDefined() 127 | const sessionRecord = SessionRecord.deserialize(record!) 128 | expect(sessionRecord.haveOpenSession()).toBeTruthy() 129 | expect(sessionRecord.getOpenSession()).toBeDefined() 130 | }) 131 | 132 | test('basic v3 NO PREKEY: the session can encrypt', async () => { 133 | const ciphertext = await aliceSessionCipher.encrypt(originalMessage) 134 | expect(ciphertext.type).toBe(3) // PREKEY_BUNDLE 135 | 136 | const plaintext = await bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext.body!, 'binary') 137 | 138 | assertEqualArrayBuffers(plaintext, originalMessage) 139 | }) 140 | 141 | test('basic v3 NO PREKEY: the session can decrypt multiple v3 messages', async () => { 142 | const ciphertext0 = await aliceSessionCipher.encrypt(originalMessage) 143 | expect(ciphertext0.type).toBe(3) // PREKEY_BUNDLE 144 | const ciphertext1 = await aliceSessionCipher.encrypt(nextMessage) 145 | expect(ciphertext1.type).toBe(3) // PREKEY_BUNDLE 146 | const plaintext0 = await bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext0.body!, 'binary') 147 | assertEqualArrayBuffers(plaintext0, originalMessage) 148 | const plaintext1 = await bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext1.body!, 'binary') 149 | assertEqualArrayBuffers(plaintext1, nextMessage) 150 | }) 151 | 152 | test('basic v3 NO PREKEY: the session can decrypt', async () => { 153 | const ciphertext = await bobSessionCipher.encrypt(originalMessage) 154 | const plaintext = await aliceSessionCipher.decryptWhisperMessage(ciphertext.body!, 'binary') 155 | assertEqualArrayBuffers(plaintext, originalMessage) 156 | }) 157 | 158 | test('basic v3 NO PREKEY: accepts a new preKey with the same identity', async () => { 159 | const preKeyBundle = await generatePreKeyBundle(bobStore, bobPreKeyId + 1, bobSignedKeyId + 1) 160 | delete preKeyBundle.preKey 161 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 162 | await builder.processPreKey(preKeyBundle) 163 | const record = await aliceStore.loadSession(BOB_ADDRESS.toString()) 164 | expect(record).toBeDefined() 165 | const sessionRecord = SessionRecord.deserialize(record!) 166 | expect(sessionRecord.haveOpenSession()).toBeTruthy() 167 | expect(sessionRecord.getOpenSession()).toBeDefined 168 | }) 169 | 170 | test('basic v3 NO PREKEY: rejects untrusted identity keys', async () => { 171 | const newIdentity = await KeyHelper.generateIdentityKeyPair() //.then(function (newIdentity) { 172 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 173 | await expect(async () => { 174 | await builder.processPreKey({ 175 | identityKey: newIdentity.pubKey, 176 | registrationId: 12356, 177 | signedPreKey: { 178 | keyId: 2, 179 | publicKey: new Uint8Array(33).buffer, 180 | signature: new Uint8Array(32).buffer, 181 | }, 182 | }) 183 | }).rejects.toThrow('Identity key changed') 184 | }) 185 | }) 186 | -------------------------------------------------------------------------------- /src/__test__/signal-protocol-store.test.ts: -------------------------------------------------------------------------------- 1 | import { SignalProtocolAddress } from '../signal-protocol-address' 2 | import { SignalProtocolStore } from './storage-type' 3 | import * as Internal from '../internal' 4 | import { assertEqualArrayBuffers } from '../__test-utils__/utils' 5 | import { Direction } from '../types' 6 | 7 | describe('SignalProtocolStore', function () { 8 | const store = new SignalProtocolStore() 9 | const registrationId = 1337 10 | const identityKey = { 11 | pubKey: Internal.crypto.getRandomBytes(33), 12 | privKey: Internal.crypto.getRandomBytes(32), 13 | } 14 | beforeAll(async () => { 15 | store.put('registrationId', registrationId) 16 | store.put('identityKey', identityKey) 17 | }) 18 | const keyPairPromise = Internal.crypto.createKeyPair() 19 | 20 | // testIdentityKeyStore(store, registrationId, identityKey); 21 | describe('IdentityKeyStore', function () { 22 | const number = '+5558675309' 23 | const address = new SignalProtocolAddress('+5558675309', 1) 24 | 25 | describe('getLocalRegistrationId', function () { 26 | test('retrieves my registration id', async () => { 27 | const reg = await store.getLocalRegistrationId() 28 | expect(reg).toBe(registrationId) 29 | }) 30 | }) 31 | describe('getIdentityKeyPair', function () { 32 | test('retrieves my identity key', async () => { 33 | const key = await store.getIdentityKeyPair() 34 | 35 | expect(key).toBeDefined() 36 | if (key) { 37 | // we know we get here by previous assertion 38 | assertEqualArrayBuffers(key.pubKey, identityKey.pubKey) 39 | assertEqualArrayBuffers(key.privKey, identityKey.privKey) 40 | } 41 | }) 42 | }) 43 | 44 | describe('saveIdentity', function () { 45 | test('stores identity keys', async () => { 46 | const testKey = await keyPairPromise 47 | await store.saveIdentity(address.toString(), testKey.pubKey) 48 | const key = await store.loadIdentityKey(number) 49 | expect(key).toBeDefined() 50 | if (key) { 51 | assertEqualArrayBuffers(key, testKey.pubKey) 52 | } 53 | }) 54 | }) 55 | describe('isTrustedIdentity', function () { 56 | test('returns true if a key is trusted', async () => { 57 | const testKey = await keyPairPromise 58 | await store.saveIdentity(address.toString(), testKey.pubKey) 59 | const trusted = await store.isTrustedIdentity(number, testKey.pubKey, Direction.RECEIVING) 60 | expect(trusted).toBeTruthy() 61 | }) 62 | 63 | test('returns false if a key is untrusted', async () => { 64 | const testKey = await keyPairPromise 65 | const newIdentity = Internal.crypto.getRandomBytes(33) 66 | await store.saveIdentity(address.toString(), testKey.pubKey) 67 | const trusted = await store.isTrustedIdentity(number, newIdentity, Direction.RECEIVING) 68 | expect(trusted).toBeFalsy() 69 | }) 70 | }) 71 | }) 72 | // testPreKeyStore(store); 73 | describe('PreKeyStore', function () { 74 | const number = '+5558675309' 75 | 76 | describe('storePreKey', function () { 77 | test('stores prekeys', async () => { 78 | const testKey = await keyPairPromise 79 | const address = new SignalProtocolAddress(number, 1) 80 | await store.storePreKey(address.toString(), testKey) 81 | const key = await store.loadPreKey(address.toString()) 82 | expect(key).toBeDefined() 83 | if (key) { 84 | assertEqualArrayBuffers(key.pubKey, testKey.pubKey) 85 | assertEqualArrayBuffers(key.privKey, testKey.privKey) 86 | } 87 | }) 88 | }) 89 | 90 | describe('loadPreKey', function () { 91 | test('returns prekeys that exist', async () => { 92 | const testKey = await keyPairPromise 93 | const address = new SignalProtocolAddress(number, 1) 94 | await store.storePreKey(address.toString(), testKey) 95 | const key = await store.loadPreKey(address.toString()) 96 | 97 | expect(key).toBeDefined() 98 | if (key) { 99 | assertEqualArrayBuffers(key.pubKey, testKey.pubKey) 100 | assertEqualArrayBuffers(key.privKey, testKey.privKey) 101 | } 102 | }) 103 | test('returns undefined for prekeys that do not exist', async () => { 104 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 105 | const address = new SignalProtocolAddress(number, 2) 106 | const key = await store.loadPreKey('2') 107 | expect(key).toBeUndefined() 108 | }) 109 | }) 110 | describe('removePreKey', function () { 111 | test('deletes prekeys', async () => { 112 | const testKey = await keyPairPromise 113 | 114 | const address = new SignalProtocolAddress(number, 2) 115 | await store.storePreKey(address.toString(), testKey) 116 | await store.removePreKey(address.toString()) 117 | const key = await store.loadPreKey(address.toString()) 118 | expect(key).toBeUndefined() 119 | }) 120 | }) 121 | }) 122 | describe('SignedPreKeyStore', function () { 123 | describe('storeSignedPreKey', function () { 124 | test('stores signed prekeys', async () => { 125 | const testKey = await keyPairPromise 126 | await store.storeSignedPreKey(3, testKey) 127 | const key = await store.loadSignedPreKey(3) 128 | expect(key).toBeDefined() 129 | if (key) { 130 | assertEqualArrayBuffers(key.pubKey, testKey.pubKey) 131 | assertEqualArrayBuffers(key.privKey, testKey.privKey) 132 | } 133 | }) 134 | }) 135 | 136 | describe('loadSignedPreKey', function () { 137 | test('returns prekeys that exist', async () => { 138 | const testKey = await keyPairPromise 139 | await store.storeSignedPreKey(1, testKey) 140 | const key = await store.loadSignedPreKey(1) 141 | expect(key).toBeDefined() 142 | if (key) { 143 | assertEqualArrayBuffers(key.pubKey, testKey.pubKey) 144 | assertEqualArrayBuffers(key.privKey, testKey.privKey) 145 | } 146 | }) 147 | 148 | test('returns undefined for prekeys that do not exist', async () => { 149 | const testKey = await keyPairPromise 150 | await store.storeSignedPreKey(1, testKey) 151 | const key = await store.loadSignedPreKey(2) 152 | expect(key).toBeUndefined() 153 | }) 154 | }) 155 | 156 | describe('removeSignedPreKey', function () { 157 | test('deletes signed prekeys', async () => { 158 | const testKey = await keyPairPromise 159 | await store.storeSignedPreKey(4, testKey) 160 | await store.removeSignedPreKey(4) // testKey) 161 | const key = await store.loadSignedPreKey(4) 162 | expect(key).toBeUndefined() 163 | }) 164 | }) 165 | }) 166 | //testSessionStore 167 | describe('SessionStore', function () { 168 | const address = new SignalProtocolAddress('+5558675309', 1) 169 | const number = '+5558675309' 170 | 171 | const testRecord = 'an opaque string' 172 | 173 | describe('storeSession', function () { 174 | // ...this used to store sessions encoded as array buffers, but the SDK 175 | // always stores them as strings. Changed the tests accordingly 176 | test('stores sessions -- see comment in code', async () => { 177 | await store.storeSession(address.toString(), testRecord) 178 | const record = await store.loadSession(address.toString()) 179 | expect(record).toBeDefined() 180 | if (record) { 181 | expect(testRecord).toStrictEqual(record) 182 | } 183 | }) 184 | }) 185 | describe('loadSession', function () { 186 | test('loadSession returns sessions that exist', async () => { 187 | const address = new SignalProtocolAddress(number, 1) 188 | const testRecord = 'an opaque string' 189 | // const enc = new TextEncoder() 190 | await store.storeSession(address.toString(), testRecord) 191 | const record = await store.loadSession(address.toString()) 192 | expect(record).toBeDefined() 193 | expect(record).toStrictEqual(testRecord) 194 | }) 195 | 196 | test('returns undefined for sessions that do not exist', async () => { 197 | const address = new SignalProtocolAddress(number, 2) 198 | const record = await store.loadSession(address.toString()) 199 | expect(record).toBeUndefined() 200 | }) 201 | }) 202 | describe('removeSession', function () { 203 | test('deletes sessions', async () => { 204 | const address = new SignalProtocolAddress(number, 1) 205 | // const enc = new TextEncoder() 206 | await store.storeSession(address.toString(), testRecord) 207 | await store.removeSession(address.toString()) 208 | const record = await store.loadSession(address.toString()) 209 | expect(record).toBeUndefined() 210 | }) 211 | }) 212 | describe('removeAllSessions', function () { 213 | test('removes all sessions for a number', async () => { 214 | const devices = [1, 2, 3].map(function (deviceId) { 215 | const address = new SignalProtocolAddress(number, deviceId) 216 | return address.toString() 217 | }) 218 | await devices.forEach(function (encodedNumber) { 219 | // const enc = new TextEncoder() 220 | 221 | store.storeSession(encodedNumber, testRecord + encodedNumber) 222 | }) 223 | await store.removeAllSessions(number) 224 | const records = await Promise.all(devices.map(store.loadSession.bind(store))) 225 | for (const i in records) { 226 | expect(records[i]).toBeUndefined() 227 | } 228 | }) 229 | }) 230 | }) 231 | }) 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signal Protocol Typescript Library (libsignal-protocol-typescript) 2 | 3 | Signal Protocol Typescript implementation based on [libsignal-protocol-javscript](https://github.com/signalapp/libsignal-protocol-javascript). 4 | 5 | ## Code layout 6 | 7 | ``` 8 | /lib # contains MSR's crypto library 9 | /src # TS source files 10 | /src/__test__ # Tests 11 | /src/__test-utils__ # Test Utilities 12 | ``` 13 | 14 | ## Overview 15 | 16 | A ratcheting forward secrecy protocol that works in synchronous and 17 | asynchronous messaging environments. 18 | 19 | ### PreKeys 20 | 21 | This protocol uses a concept called 'PreKeys'. A PreKey is an ECPublicKey and 22 | an associated unique ID which are stored together by a server. PreKeys can also 23 | be signed. 24 | 25 | At install time, clients generate a single signed PreKey, as well as a large 26 | list of unsigned PreKeys, and transmit all of them to the server. 27 | 28 | ### Sessions 29 | 30 | Signal Protocol is session-oriented. Clients establish a "session," which is 31 | then used for all subsequent encrypt/decrypt operations. There is no need to 32 | ever tear down a session once one has been established. 33 | 34 | Sessions are established in one of two ways: 35 | 36 | 1. PreKeyBundles. A client that wishes to send a message to a recipient can 37 | establish a session by retrieving a PreKeyBundle for that recipient from the 38 | server. 39 | 1. PreKeySignalMessages. A client can receive a PreKeySignalMessage from a 40 | recipient and use it to establish a session. 41 | 42 | ### State 43 | 44 | An established session encapsulates a lot of state between two clients. That 45 | state is maintained in durable records which need to be kept for the life of 46 | the session. 47 | 48 | State is kept in the following places: 49 | 50 | - Identity State. Clients will need to maintain the state of their own identity 51 | key pair, as well as identity keys received from other clients. 52 | - PreKey State. Clients will need to maintain the state of their generated 53 | PreKeys. 54 | - Signed PreKey States. Clients will need to maintain the state of their signed 55 | PreKeys. 56 | - Session State. Clients will need to maintain the state of the sessions they 57 | have established. 58 | 59 | ## Usage 60 | 61 | The code samples below come almost directly from our [sample web application](https://github.com/privacyresearchgroup/libsignal-typescript-demo). Please have a look there to see how everything fits together. Look at this project's unit tests too. 62 | 63 | ### Add the SDK to your project 64 | 65 | We use [yarn](https://yarnpkg.com). 66 | 67 | ``` 68 | yarn add @privacyresearch/libsignal-protocol-typescript 69 | ``` 70 | 71 | But npm is good too: 72 | 73 | ``` 74 | npm install @privacyresearch/libsignal-protocol-typescript 75 | ``` 76 | 77 | Now you can import classes and functions from the library. To make the examples below work, the following import suffices: 78 | 79 | ``` 80 | 81 | import { 82 | KeyHelper, 83 | SignedPublicPreKeyType, 84 | SignalProtocolAddress, 85 | SessionBuilder, 86 | PreKeyType, 87 | SessionCipher, 88 | MessageType } 89 | from '@privacyresearch/libsignal-protocol-typescript' 90 | ``` 91 | 92 | If you prefer to use a prefix like `libsignal` and keep a short import, you can do the following: 93 | 94 | ``` 95 | import * as libsignal from '@privacyresearch/libsignal-protocol-typescript' 96 | ``` 97 | 98 | #### Install time 99 | 100 | At install time, a signal client needs to generate its identity keys, 101 | registration id, and prekeys. 102 | 103 | A signal client also needs to implement a storage interface that will manage 104 | loading and storing of identity, prekeys, signed prekeys, and session state. 105 | See [`src/__test__/storage-type.ts`]() for an example. 106 | 107 | Here is what setup might look like: 108 | 109 | ```ts 110 | const createID = async (name: string, store: SignalProtocolStore) => { 111 | const registrationId = KeyHelper.generateRegistrationId() 112 | storeSomewhereSafe(`registrationID`, registrationId) 113 | 114 | const identityKeyPair = await KeyHelper.generateIdentityKeyPair() 115 | storeSomewhereSafe('identityKey', identityKeyPair) 116 | 117 | const baseKeyId = makeKeyId() 118 | const preKey = await KeyHelper.generatePreKey(baseKeyId) 119 | store.storePreKey(`${baseKeyId}`, preKey.keyPair) 120 | 121 | const signedPreKeyId = makeKeyId() 122 | const signedPreKey = await KeyHelper.generateSignedPreKey(identityKeyPair, signedPreKeyId) 123 | store.storeSignedPreKey(signedPreKeyId, signedPreKey.keyPair) 124 | 125 | // Now we register this with the server or other directory so all users can see them. 126 | // You might implement your directory differently, this is not part of the SDK. 127 | 128 | const publicSignedPreKey: SignedPublicPreKeyType = { 129 | keyId: signedPreKeyId, 130 | publicKey: signedPreKey.keyPair.pubKey, 131 | signature: signedPreKey.signature, 132 | } 133 | 134 | const publicPreKey: PreKeyType = { 135 | keyId: preKey.keyId, 136 | publicKey: preKey.keyPair.pubKey, 137 | } 138 | 139 | directory.storeKeyBundle(name, { 140 | registrationId, 141 | identityPubKey: identityKeyPair.pubKey, 142 | signedPreKey: publicSignedPreKey, 143 | oneTimePreKeys: [publicPreKey], 144 | }) 145 | } 146 | ``` 147 | 148 | Relevant type definitions and classes: [KeyHelper](), [KeyPairType](), [PreKeyPairType](), [SignedPreKeyPairType](), 149 | [PreKeyType](), [SignedPublicPreKeyType](). 150 | 151 | ### Building a session 152 | 153 | Once this is implemented, building a session is fairly straightforward: 154 | 155 | ```ts 156 | const starterMessageBytes = Uint8Array.from([ 157 | 0xce, 158 | 0x93, 159 | 0xce, 160 | 0xb5, 161 | 0xce, 162 | 0xb9, 163 | 0xce, 164 | 0xac, 165 | 0x20, 166 | 0xcf, 167 | 0x83, 168 | 0xce, 169 | 0xbf, 170 | 0xcf, 171 | 0x85, 172 | ]) 173 | 174 | const startSessionWithBoris = async () => { 175 | // get Boris' key bundle. This is a DeviceType 176 | const borisBundle = directory.getPreKeyBundle('boris') 177 | 178 | // borisAddress is a SignalProtocolAddress 179 | const recipientAddress = borisAddress 180 | 181 | // Instantiate a SessionBuilder for a remote recipientId + deviceId tuple. 182 | const sessionBuilder = new SessionBuilder(adiStore, recipientAddress) 183 | 184 | // Process a prekey fetched from the server. Returns a promise that resolves 185 | // once a session is created and saved in the store, or rejects if the 186 | // identityKey differs from a previously seen identity for this address. 187 | await sessionBuilder.processPreKey(borisBundle!) 188 | 189 | // Now we can encrypt a messageto get a MessageType object 190 | const senderSessionCipher = new SessionCipher(adiStore, recipientAddress) 191 | const ciphertext = await senderSessionCipher.encrypt(starterMessageBytes.buffer) 192 | 193 | // The message is encrypted, now send it however you like. 194 | sendMessage('boris', 'adalheid', ciphertext) 195 | } 196 | ``` 197 | 198 | Relevant type definitions: [DeviceType](), [SignalProtocolAddress](), [MessageType](), [SessionBuilder](), [SessionCipher]() 199 | 200 | _Note:_ As discussed below, the Signal protocol uses two message types: `PreKeyWhisperMessage` and `WhisperMessage` that are defined 201 | in [the protobuf definitions]() and implemented in [libsignal-protocol-protobuf-ts](https://github.com/privacyresearchgroup/libsignal-protocol-protobuf-ts). The message created in the sample above is a `PreKeyWhisperMessage`. It carries information needed for the recipient to build a session with the [X3DH Protocol](https://signal.org/docs/specifications/x3dh/). After a session is established for a recipient, `SessionCipher.encrypt()` will return a simpler `WhisperMessage`. 202 | 203 | > **\*Into the weeds:** The function `sessionCipher.encrypt()` always returns a [`MessageType`]() object. Sometimes it is a `PreKeyWhisperMessage` and sometimes it is a `WhisperMessage`. To distinguish, check `ciphertext.type`. If `ciphertext.type === 3` then `ciphertext.body` contains a serialized `PreKeyWhisperMessage`. If `ciphertext.type === 1` then `ciphertext.body` contains a serialized `WhisperMessage`.\* 204 | 205 | ### Encrypting 206 | 207 | Once you have a session established with an address, you can encrypt messages 208 | using SessionCipher. 209 | 210 | ```ts 211 | const plaintext = 'μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος / οὐλομένην, ἣ μυρί᾽ Ἀχαιοῖς ἄλγε᾽ ἔθηκε' 212 | const buffer = new TextEncoder().encode(plaintext).buffer 213 | 214 | const sessionCipher = new SessionCipher(store, address) 215 | const ciphertext = await sessionCipher.encrypt(buffer) 216 | // If we've already established a session, thenciphertext.type === 1. 217 | 218 | // Now we can send it over the channel of our choice 219 | sendMessage('adalheid', 'boris', ciphertext) 220 | ``` 221 | 222 | ### Decrypting 223 | 224 | Ciphertexts come in two flavors: WhisperMessage and PreKeyWhisperMessage. 225 | 226 | ```ts 227 | const address = new SignalProtocolAddress(recipientId, deviceId) 228 | const sessionCipher = new SessionCipher(store, address) 229 | 230 | // Decrypting a PreKeyWhisperMessage will establish a new session and 231 | // store it in the SignalProtocolStore. It returns a promise that resolves 232 | // when the message is decrypted or rejects if the identityKey differs from 233 | // a previously seen identity for this address. 234 | 235 | let plaintext: ArrayBuffer 236 | // ciphertext: MessageType 237 | if (ciphertext.type === 3) { 238 | // It is a PreKeyWhisperMessage and will establish a session. 239 | try { 240 | plaintext = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext.body!, 'binary') 241 | } catch (e) { 242 | // handle identity key conflict 243 | } 244 | } else if (ciphertext.type === 1) { 245 | // It is a WhisperMessage for an established session. 246 | plaintext = await sessionCipher.decryptWhisperMessage(ciphertext.body!, 'binary') 247 | } 248 | 249 | // now you can do something with your plaintext, like 250 | const secretMessage = new TextDecoder().decode(new Uint8Array(plaintext)) 251 | ``` 252 | 253 | ## Injecting Dependencies 254 | 255 | This library uses [WebCrypto]() for symmetric key cryptography and random number generation. It uses an implemenation of the [AsyncCurve](https://github.com/privacyresearchgroup/curve25519-typescript/blob/master/src/types.ts#L21) interface in [`curve25519-typescript`](https://github.com/privacyresearchgroup/curve25519-typescript) for public key operations. 256 | 257 | Functional defaults are provided for each but you may want to provide your own, either for performance or security reasons. 258 | 259 | ### WebCrypto defaults and injection 260 | 261 | By default this library will use `window.crypto` if it is present. Otherwise it uses [`msrcrypto`](https://www.npmjs.com/package/msrcrypto). If you are falling back to `msrcrypto` you will want to consider providing a substitute. 262 | 263 | To replace the WebCrypto component with your own, simply call `setWebCrypto` as follows: 264 | 265 | ```ts 266 | setWebCrypto(myCryptImplementation) 267 | ``` 268 | 269 | Your WebCrypto imlementation does not need to support the entire interface, but does need to implement: 270 | 271 | - AES-CBC 272 | - HMAC SHA-256 273 | - `getRandomValues` 274 | 275 | ### Elliptic curve crypto defaults and injection 276 | 277 | By default this library uses the curve X25519 implementation in [`curve25519-typescript`](https://github.com/privacyresearchgroup/curve25519-typescript). This is a javascript implementation, compiled into [asm.js](http://asmjs.org/) from C with [emscripten](https://emscripten.org/). You may want to provide a native implementation or even use a different curve, like X448. To do this, wrap your implementation into a an object that implements the [AsyncCurve](https://github.com/privacyresearchgroup/curve25519-typescript/blob/master/src/types.ts#L21) interface and set it as follows: 278 | 279 | ```ts 280 | setCurve(myCurve) 281 | ``` 282 | 283 | ## License 284 | 285 | Copyright 2020 by Privacy Research, LLC 286 | 287 | Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html 288 | -------------------------------------------------------------------------------- /src/session-builder.ts: -------------------------------------------------------------------------------- 1 | import { SignalProtocolAddressType, StorageType, Direction, KeyPairType } from './types' 2 | import { DeviceType, SessionType, BaseKeyType, ChainType } from './session-types' 3 | 4 | import * as Internal from './internal' 5 | import * as base64 from 'base64-js' 6 | import { SessionRecord } from './session-record' 7 | import { PreKeyWhisperMessage } from '@privacyresearch/libsignal-protocol-protobuf-ts' 8 | import { SessionLock } from './session-lock' 9 | import { uint8ArrayToArrayBuffer } from './helpers' 10 | 11 | export class SessionBuilder { 12 | remoteAddress: SignalProtocolAddressType 13 | storage: StorageType 14 | 15 | constructor(storage: StorageType, remoteAddress: SignalProtocolAddressType) { 16 | this.remoteAddress = remoteAddress 17 | this.storage = storage 18 | } 19 | 20 | processPreKeyJob = async (device: DeviceType): Promise => { 21 | const trusted = await this.storage.isTrustedIdentity( 22 | this.remoteAddress.name, 23 | device.identityKey, 24 | Direction.SENDING 25 | ) 26 | if (!trusted) { 27 | throw new Error('Identity key changed') 28 | } 29 | 30 | // This will throw if invalid 31 | await Internal.crypto.Ed25519Verify( 32 | device.identityKey, 33 | device.signedPreKey.publicKey, 34 | device.signedPreKey.signature 35 | ) 36 | 37 | const ephemeralKey = await Internal.crypto.createKeyPair() 38 | 39 | const deviceOneTimePreKey = device.preKey?.publicKey 40 | 41 | const session = await this.startSessionAsInitiator( 42 | ephemeralKey, 43 | device.identityKey, 44 | device.signedPreKey.publicKey, 45 | deviceOneTimePreKey, 46 | device.registrationId 47 | ) 48 | session.pendingPreKey = { 49 | signedKeyId: device.signedPreKey.keyId, 50 | baseKey: ephemeralKey.pubKey, 51 | } 52 | if (device.preKey) { 53 | session.pendingPreKey.preKeyId = device.preKey.keyId 54 | } 55 | const address = this.remoteAddress.toString() 56 | const serialized = await this.storage.loadSession(address) 57 | let record: SessionRecord 58 | if (serialized !== undefined) { 59 | record = SessionRecord.deserialize(serialized) 60 | } else { 61 | record = new SessionRecord() 62 | } 63 | 64 | record.archiveCurrentState() 65 | record.updateSessionState(session) 66 | await Promise.all([ 67 | this.storage.storeSession(address, record.serialize()), 68 | this.storage.saveIdentity(this.remoteAddress.toString(), session.indexInfo.remoteIdentityKey), 69 | ]) 70 | 71 | return session 72 | } 73 | 74 | // Arguments map to the X3DH spec: https://signal.org/docs/specifications/x3dh/#keys 75 | // We are Alice the initiator. 76 | startSessionAsInitiator = async ( 77 | EKa: KeyPairType, 78 | IKb: ArrayBuffer, 79 | SPKb: ArrayBuffer, 80 | OPKb: ArrayBuffer | undefined, 81 | registrationId?: number 82 | ): Promise => { 83 | const IKa = await this.storage.getIdentityKeyPair() 84 | 85 | if (!IKa) { 86 | throw new Error(`No identity key. Cannot initiate session.`) 87 | } 88 | 89 | let sharedSecret: Uint8Array 90 | if (OPKb === undefined) { 91 | sharedSecret = new Uint8Array(32 * 4) 92 | } else { 93 | sharedSecret = new Uint8Array(32 * 5) 94 | } 95 | 96 | // As specified in X3DH spec secion 22, the first 32 bytes are 97 | // 0xFF for curve25519 (https://signal.org/docs/specifications/x3dh/#cryptographic-notation) 98 | for (let i = 0; i < 32; i++) { 99 | sharedSecret[i] = 0xff 100 | } 101 | 102 | if (!SPKb) { 103 | throw new Error(`theirSignedPubKey is undefined. Cannot proceed with ECDHE`) 104 | } 105 | 106 | // X3DH Section 3.3. https://signal.org/docs/specifications/x3dh/ 107 | // We'll handle the possible one-time prekey below 108 | const ecRes = await Promise.all([ 109 | Internal.crypto.ECDHE(SPKb, IKa.privKey), 110 | Internal.crypto.ECDHE(IKb, EKa.privKey), 111 | Internal.crypto.ECDHE(SPKb, EKa.privKey), 112 | ]) 113 | 114 | sharedSecret.set(new Uint8Array(ecRes[0]), 32) 115 | sharedSecret.set(new Uint8Array(ecRes[1]), 32 * 2) 116 | 117 | sharedSecret.set(new Uint8Array(ecRes[2]), 32 * 3) 118 | 119 | if (OPKb !== undefined) { 120 | const ecRes4 = await Internal.crypto.ECDHE(OPKb, EKa.privKey) 121 | sharedSecret.set(new Uint8Array(ecRes4), 32 * 4) 122 | } 123 | 124 | const masterKey = await Internal.HKDF(uint8ArrayToArrayBuffer(sharedSecret), new ArrayBuffer(32), 'WhisperText') 125 | 126 | const session: SessionType = { 127 | registrationId: registrationId, 128 | currentRatchet: { 129 | rootKey: masterKey[0], 130 | lastRemoteEphemeralKey: SPKb, 131 | previousCounter: 0, 132 | }, 133 | indexInfo: { 134 | remoteIdentityKey: IKb, 135 | closed: -1, 136 | }, 137 | oldRatchetList: [], 138 | chains: {}, 139 | } 140 | 141 | // We're initiating so we go ahead and set our first sending ephemeral key now, 142 | // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key 143 | 144 | session.indexInfo.baseKey = EKa.pubKey 145 | session.indexInfo.baseKeyType = BaseKeyType.OURS 146 | const ourSendingEphemeralKey = await Internal.crypto.createKeyPair() 147 | session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey 148 | 149 | await this.calculateSendingRatchet(session, SPKb) 150 | 151 | return session 152 | } 153 | 154 | // Arguments map to the X3DH spec: https://signal.org/docs/specifications/x3dh/#keys 155 | // We are Bob now. 156 | startSessionWthPreKeyMessage = async ( 157 | OPKb: KeyPairType | undefined, 158 | SPKb: KeyPairType, 159 | message: PreKeyWhisperMessage 160 | ): Promise => { 161 | const IKb = await this.storage.getIdentityKeyPair() 162 | const IKa = message.identityKey 163 | const EKa = message.baseKey 164 | 165 | if (!IKb) { 166 | throw new Error(`No identity key. Cannot initiate session.`) 167 | } 168 | 169 | let sharedSecret: Uint8Array 170 | if (!OPKb) { 171 | sharedSecret = new Uint8Array(32 * 4) 172 | } else { 173 | sharedSecret = new Uint8Array(32 * 5) 174 | } 175 | 176 | // As specified in X3DH spec secion 22, the first 32 bytes are 177 | // 0xFF for curve25519 (https://signal.org/docs/specifications/x3dh/#cryptographic-notation) 178 | for (let i = 0; i < 32; i++) { 179 | sharedSecret[i] = 0xff 180 | } 181 | 182 | // X3DH Section 3.3. https://signal.org/docs/specifications/x3dh/ 183 | // We'll handle the possible one-time prekey below 184 | const ecRes = await Promise.all([ 185 | Internal.crypto.ECDHE(IKa, SPKb.privKey), 186 | Internal.crypto.ECDHE(EKa, IKb.privKey), 187 | Internal.crypto.ECDHE(EKa, SPKb.privKey), 188 | ]) 189 | 190 | sharedSecret.set(new Uint8Array(ecRes[0]), 32) 191 | sharedSecret.set(new Uint8Array(ecRes[1]), 32 * 2) 192 | sharedSecret.set(new Uint8Array(ecRes[2]), 32 * 3) 193 | 194 | if (OPKb) { 195 | const ecRes4 = await Internal.crypto.ECDHE(EKa, OPKb.privKey) 196 | sharedSecret.set(new Uint8Array(ecRes4), 32 * 4) 197 | } 198 | 199 | const masterKey = await Internal.HKDF(uint8ArrayToArrayBuffer(sharedSecret), new ArrayBuffer(32), 'WhisperText') 200 | 201 | const session: SessionType = { 202 | registrationId: message.registrationId, 203 | currentRatchet: { 204 | rootKey: masterKey[0], 205 | lastRemoteEphemeralKey: EKa, 206 | previousCounter: 0, 207 | }, 208 | indexInfo: { 209 | remoteIdentityKey: IKa, 210 | closed: -1, 211 | }, 212 | oldRatchetList: [], 213 | chains: {}, 214 | } 215 | 216 | // If we're initiating we go ahead and set our first sending ephemeral key now, 217 | // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key 218 | 219 | session.indexInfo.baseKey = EKa 220 | session.indexInfo.baseKeyType = BaseKeyType.THEIRS 221 | session.currentRatchet.ephemeralKeyPair = SPKb 222 | 223 | return session 224 | } 225 | 226 | async calculateSendingRatchet(session: SessionType, remoteKey: ArrayBuffer): Promise { 227 | const ratchet = session.currentRatchet 228 | if (!ratchet.ephemeralKeyPair) { 229 | throw new Error(`Invalid ratchet - ephemeral key pair is missing`) 230 | } 231 | 232 | const ephPrivKey = ratchet.ephemeralKeyPair.privKey 233 | const rootKey = ratchet.rootKey 234 | const ephPubKey = base64.fromByteArray(new Uint8Array(ratchet.ephemeralKeyPair.pubKey)) 235 | if (!(ephPrivKey && ephPubKey && rootKey)) { 236 | throw new Error(`Missing key, cannot calculate sending ratchet`) 237 | } 238 | const sharedSecret = await Internal.crypto.ECDHE(remoteKey, ephPrivKey) 239 | const masterKey = await Internal.HKDF(sharedSecret, rootKey, 'WhisperRatchet') 240 | 241 | session.chains[ephPubKey] = { 242 | messageKeys: {}, 243 | chainKey: { counter: -1, key: masterKey[1] }, 244 | chainType: ChainType.SENDING, 245 | } 246 | ratchet.rootKey = masterKey[0] 247 | } 248 | 249 | async processPreKey(device: DeviceType): Promise { 250 | // return this.processPreKeyJob(device) 251 | const runJob = async () => { 252 | const sess = await this.processPreKeyJob(device) 253 | return sess 254 | } 255 | return SessionLock.queueJobForNumber(this.remoteAddress.toString(), runJob) 256 | } 257 | 258 | async processV3(record: SessionRecord, message: PreKeyWhisperMessage): Promise { 259 | const trusted = this.storage.isTrustedIdentity( 260 | this.remoteAddress.name, 261 | uint8ArrayToArrayBuffer(message.identityKey), 262 | Direction.RECEIVING 263 | ) 264 | 265 | if (!trusted) { 266 | throw new Error(`Unknown identity key: ${uint8ArrayToArrayBuffer(message.identityKey)}`) 267 | } 268 | const [preKeyPair, signedPreKeyPair] = await Promise.all([ 269 | this.storage.loadPreKey(message.preKeyId), 270 | this.storage.loadSignedPreKey(message.signedPreKeyId), 271 | ]) 272 | 273 | if (record.getSessionByBaseKey(message.baseKey)) { 274 | return 275 | } 276 | 277 | const session = record.getOpenSession() 278 | 279 | if (signedPreKeyPair === undefined) { 280 | // Session may or may not be the right one, but if its not, we 281 | // can't do anything about it ...fall through and let 282 | // decryptWhisperMessage handle that case 283 | if (session !== undefined && session.currentRatchet !== undefined) { 284 | return 285 | } else { 286 | throw new Error('Missing Signed PreKey for PreKeyWhisperMessage') 287 | } 288 | } 289 | 290 | if (session !== undefined) { 291 | record.archiveCurrentState() 292 | } 293 | if (message.preKeyId && !preKeyPair) { 294 | // console.log('Invalid prekey id', message.preKeyId) 295 | } 296 | 297 | const new_session = await this.startSessionWthPreKeyMessage(preKeyPair, signedPreKeyPair, message) 298 | record.updateSessionState(new_session) 299 | await this.storage.saveIdentity(this.remoteAddress.toString(), uint8ArrayToArrayBuffer(message.identityKey)) 300 | 301 | return message.preKeyId 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/session-record.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | 3 | import base64 from 'base64-js' 4 | 5 | import * as util from './helpers' 6 | import { KeyPairType } from './types' 7 | import { 8 | SessionType, 9 | BaseKeyType, 10 | PendingPreKey, 11 | Chain, 12 | OldRatchetInfo, 13 | Ratchet, 14 | IndexInfo, 15 | RecordType, 16 | } from './session-types' 17 | 18 | const ARCHIVED_STATES_MAX_LENGTH = 40 19 | const OLD_RATCHETS_MAX_LENGTH = 10 20 | const SESSION_RECORD_VERSION = 'v1' 21 | 22 | export class SessionRecord implements RecordType { 23 | private static migrations = [ 24 | { 25 | version: 'v1', 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | migrate: function migrateV1(data: any) { 28 | const sessions = data.sessions 29 | let key 30 | if (data.registrationId) { 31 | for (key in sessions) { 32 | if (!sessions[key].registrationId) { 33 | sessions[key].registrationId = data.registrationId 34 | } 35 | } 36 | } else { 37 | for (key in sessions) { 38 | if (sessions[key].indexInfo.closed === -1) { 39 | // console.log( 40 | // 'V1 session storage migration error: registrationId', 41 | // data.registrationId, 42 | // 'for open session version', 43 | // data.version 44 | // ) 45 | } 46 | } 47 | } 48 | }, 49 | }, 50 | ] 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | private static migrate(data: any): void { 54 | let run = data.version === undefined 55 | for (let i = 0; i < SessionRecord.migrations.length; ++i) { 56 | if (run) { 57 | SessionRecord.migrations[i].migrate(data) 58 | } else if (SessionRecord.migrations[i].version === data.version) { 59 | run = true 60 | } 61 | } 62 | if (!run) { 63 | throw new Error('Error migrating SessionRecord') 64 | } 65 | } 66 | 67 | registrationId?: number 68 | sessions: { [k: string]: SessionType } = {} 69 | version = SESSION_RECORD_VERSION 70 | constructor(registrationId?: number) { 71 | this.registrationId = registrationId 72 | } 73 | 74 | static deserialize(serialized: string): SessionRecord { 75 | const data = JSON.parse(serialized) 76 | if (data.version !== SESSION_RECORD_VERSION) { 77 | SessionRecord.migrate(data) 78 | } 79 | 80 | const record = new SessionRecord() 81 | record.sessions = {} 82 | for (const k of Object.keys(data.sessions)) { 83 | record.sessions[k] = sessionTypeStringToArrayBuffer(data.sessions[k]) 84 | } 85 | if ( 86 | record.sessions === undefined || 87 | record.sessions === null || 88 | typeof record.sessions !== 'object' || 89 | Array.isArray(record.sessions) 90 | ) { 91 | throw new Error('Error deserializing SessionRecord') 92 | } 93 | return record 94 | } 95 | 96 | serialize(): string { 97 | const sessions: { [k: string]: SessionType } = {} 98 | for (const k of Object.keys(this.sessions)) { 99 | sessions[k] = sessionTypeArrayBufferToString(this.sessions[k]) 100 | } 101 | const json = { 102 | sessions, 103 | version: this.version, 104 | } 105 | return JSON.stringify(json) 106 | } 107 | 108 | haveOpenSession(): boolean { 109 | const openSession = this.getOpenSession() 110 | return !!openSession && typeof openSession.registrationId === 'number' 111 | } 112 | 113 | getSessionByBaseKey(baseKey: ArrayBuffer): SessionType | undefined { 114 | const idx = util.arrayBufferToString(baseKey) 115 | if (!idx) { 116 | return undefined 117 | } 118 | const session = this.sessions[idx] 119 | if (session && session.indexInfo.baseKeyType === BaseKeyType.OURS) { 120 | return undefined 121 | } 122 | return session 123 | } 124 | 125 | getSessionByRemoteEphemeralKey(remoteEphemeralKey: ArrayBuffer): SessionType | undefined { 126 | this.detectDuplicateOpenSessions() 127 | const sessions = this.sessions 128 | 129 | const searchKey = util.arrayBufferToString(remoteEphemeralKey) 130 | 131 | if (searchKey) { 132 | let openSession 133 | for (const key in sessions) { 134 | if (sessions[key].indexInfo.closed == -1) { 135 | openSession = sessions[key] 136 | } 137 | if (sessions[key].chains[searchKey] !== undefined) { 138 | return sessions[key] 139 | } 140 | } 141 | if (openSession !== undefined) { 142 | return openSession 143 | } 144 | } 145 | 146 | return undefined 147 | } 148 | 149 | getOpenSession(): SessionType | undefined { 150 | const sessions = this.sessions 151 | if (sessions === undefined) { 152 | return undefined 153 | } 154 | 155 | this.detectDuplicateOpenSessions() 156 | 157 | for (const key in sessions) { 158 | if (sessions[key].indexInfo.closed == -1) { 159 | return sessions[key] 160 | } 161 | } 162 | return undefined 163 | } 164 | 165 | private detectDuplicateOpenSessions(): void { 166 | let openSession: SessionType | null = null 167 | const sessions = this.sessions 168 | for (const key in sessions) { 169 | if (sessions[key].indexInfo.closed == -1) { 170 | if (openSession !== null) { 171 | throw new Error('Datastore inconsistensy: multiple open sessions') 172 | } 173 | openSession = sessions[key] 174 | } 175 | } 176 | } 177 | 178 | updateSessionState(session: SessionType): void { 179 | const sessions = this.sessions 180 | 181 | this.removeOldChains(session) 182 | 183 | const idx = session.indexInfo.baseKey && util.arrayBufferToString(session.indexInfo.baseKey) 184 | if (!idx) { 185 | throw new Error(`invalid index for session`) 186 | } 187 | sessions[idx] = session 188 | 189 | this.removeOldSessions() 190 | } 191 | 192 | getSessions(): SessionType[] { 193 | // return an array of sessions ordered by time closed, 194 | // followed by the open session 195 | let list: SessionType[] = [] 196 | let openSession: SessionType | null = null 197 | for (const k in this.sessions) { 198 | if (this.sessions[k].indexInfo.closed === -1) { 199 | openSession = this.sessions[k] 200 | } else { 201 | list.push(this.sessions[k]) 202 | } 203 | } 204 | list = list.sort(function (s1, s2) { 205 | return s1.indexInfo.closed - s2.indexInfo.closed 206 | }) 207 | if (openSession) { 208 | list.push(openSession) 209 | } 210 | return list 211 | } 212 | 213 | archiveCurrentState(): void { 214 | const open_session = this.getOpenSession() 215 | if (open_session !== undefined) { 216 | open_session.indexInfo.closed = Date.now() 217 | this.updateSessionState(open_session) 218 | } 219 | } 220 | promoteState(session: SessionType): void { 221 | session.indexInfo.closed = -1 222 | } 223 | 224 | removeOldChains(session: SessionType): void { 225 | // Sending ratchets are always removed when we step because we never need them again 226 | // Receiving ratchets are added to the oldRatchetList, which we parse 227 | // here and remove all but the last ten. 228 | while (session.oldRatchetList.length > OLD_RATCHETS_MAX_LENGTH) { 229 | let index = 0 230 | let oldest = session.oldRatchetList[0] 231 | for (let i = 0; i < session.oldRatchetList.length; i++) { 232 | if (session.oldRatchetList[i].added < oldest.added) { 233 | oldest = session.oldRatchetList[i] 234 | index = i 235 | } 236 | } 237 | const idx = util.arrayBufferToString(oldest.ephemeralKey) 238 | if (!idx) { 239 | throw new Error(`invalid index for chain`) 240 | } 241 | delete session[idx] 242 | session.oldRatchetList.splice(index, 1) 243 | } 244 | } 245 | 246 | removeOldSessions(): void { 247 | // Retain only the last 20 sessions 248 | const { sessions } = this 249 | let oldestBaseKey: string | null = null 250 | let oldestSession: SessionType | null = null 251 | while (Object.keys(sessions).length > ARCHIVED_STATES_MAX_LENGTH) { 252 | for (const key in sessions) { 253 | const session = sessions[key] 254 | if ( 255 | session.indexInfo.closed > -1 && // session is closed 256 | (!oldestSession || session.indexInfo.closed < oldestSession.indexInfo.closed) 257 | ) { 258 | oldestBaseKey = key 259 | oldestSession = session 260 | } 261 | } 262 | if (oldestBaseKey) { 263 | delete sessions[oldestBaseKey] 264 | } 265 | } 266 | } 267 | deleteAllSessions(): void { 268 | // Used primarily in session reset scenarios, where we really delete sessions 269 | this.sessions = {} 270 | } 271 | } 272 | 273 | // Serialization helpers 274 | function toAB(s: string): ArrayBuffer { 275 | return util.uint8ArrayToArrayBuffer(base64.toByteArray(s)) 276 | } 277 | function abToS(b: ArrayBuffer): string { 278 | return base64.fromByteArray(new Uint8Array(b)) 279 | } 280 | 281 | export function keyPairStirngToArrayBuffer(kp: KeyPairType): KeyPairType { 282 | return { 283 | pubKey: toAB(kp.pubKey), 284 | privKey: toAB(kp.privKey), 285 | } 286 | } 287 | 288 | export function keyPairArrayBufferToString(kp: KeyPairType): KeyPairType { 289 | return { 290 | pubKey: abToS(kp.pubKey), 291 | privKey: abToS(kp.privKey), 292 | } 293 | } 294 | 295 | export function pendingPreKeyStringToArrayBuffer(ppk: PendingPreKey): PendingPreKey { 296 | const { preKeyId, signedKeyId } = ppk 297 | return { 298 | baseKey: toAB(ppk.baseKey), 299 | preKeyId, 300 | signedKeyId, 301 | } 302 | } 303 | 304 | export function pendingPreKeyArrayBufferToString(ppk: PendingPreKey): PendingPreKey { 305 | const { preKeyId, signedKeyId } = ppk 306 | return { 307 | baseKey: abToS(ppk.baseKey), 308 | preKeyId, 309 | signedKeyId, 310 | } 311 | } 312 | 313 | export function chainStringToArrayBuffer(c: Chain): Chain { 314 | const { chainType, chainKey, messageKeys } = c 315 | const { key, counter } = chainKey 316 | const newMessageKeys: { [k: number]: ArrayBuffer } = {} 317 | for (const k of Object.keys(messageKeys)) { 318 | newMessageKeys[k] = toAB(messageKeys[k]) 319 | } 320 | return { 321 | chainType, 322 | chainKey: { 323 | key: key ? util.uint8ArrayToArrayBuffer(base64.toByteArray(key)) : undefined, 324 | counter, 325 | }, 326 | messageKeys: newMessageKeys, 327 | } 328 | } 329 | 330 | export function chainArrayBufferToString(c: Chain): Chain { 331 | const { chainType, chainKey, messageKeys } = c 332 | const { key, counter } = chainKey 333 | const newMessageKeys: { [k: number]: string } = {} 334 | for (const k of Object.keys(messageKeys)) { 335 | newMessageKeys[k] = abToS(messageKeys[k]) 336 | } 337 | return { 338 | chainType, 339 | chainKey: { 340 | key: key ? abToS(key) : undefined, 341 | counter, 342 | }, 343 | messageKeys: newMessageKeys, 344 | } 345 | } 346 | 347 | export function oldRatchetInfoStringToArrayBuffer(ori: OldRatchetInfo): OldRatchetInfo { 348 | return { 349 | ephemeralKey: toAB(ori.ephemeralKey), 350 | added: ori.added, 351 | } 352 | } 353 | 354 | export function oldRatchetInfoArrayBufferToString(ori: OldRatchetInfo): OldRatchetInfo { 355 | return { 356 | ephemeralKey: abToS(ori.ephemeralKey), 357 | added: ori.added, 358 | } 359 | } 360 | 361 | export function ratchetStringToArrayBuffer(r: Ratchet): Ratchet { 362 | return { 363 | rootKey: toAB(r.rootKey), 364 | ephemeralKeyPair: r.ephemeralKeyPair && keyPairStirngToArrayBuffer(r.ephemeralKeyPair), 365 | lastRemoteEphemeralKey: toAB(r.lastRemoteEphemeralKey), 366 | previousCounter: r.previousCounter, 367 | added: r.added, 368 | } 369 | } 370 | 371 | export function ratchetArrayBufferToString(r: Ratchet): Ratchet { 372 | return { 373 | rootKey: abToS(r.rootKey), 374 | ephemeralKeyPair: r.ephemeralKeyPair && keyPairArrayBufferToString(r.ephemeralKeyPair), 375 | lastRemoteEphemeralKey: abToS(r.lastRemoteEphemeralKey), 376 | previousCounter: r.previousCounter, 377 | added: r.added, 378 | } 379 | } 380 | 381 | export function indexInfoStringToArrayBuffer(ii: IndexInfo): IndexInfo { 382 | const { closed, remoteIdentityKey, baseKey, baseKeyType } = ii 383 | return { 384 | closed, 385 | remoteIdentityKey: toAB(remoteIdentityKey), 386 | baseKey: baseKey ? toAB(baseKey) : undefined, 387 | baseKeyType, 388 | } 389 | } 390 | 391 | export function indexInfoArrayBufferToString(ii: IndexInfo): IndexInfo { 392 | const { closed, remoteIdentityKey, baseKey, baseKeyType } = ii 393 | return { 394 | closed, 395 | remoteIdentityKey: abToS(remoteIdentityKey), 396 | baseKey: baseKey ? abToS(baseKey) : undefined, 397 | baseKeyType, 398 | } 399 | } 400 | 401 | export function sessionTypeStringToArrayBuffer(sess: SessionType): SessionType { 402 | const { indexInfo, registrationId, currentRatchet, pendingPreKey, oldRatchetList, chains } = sess 403 | const newChains: { [ephKeyString: string]: Chain } = {} 404 | for (const k of Object.keys(chains)) { 405 | newChains[k] = chainStringToArrayBuffer(chains[k]) 406 | } 407 | return { 408 | indexInfo: indexInfoStringToArrayBuffer(indexInfo), 409 | registrationId, 410 | currentRatchet: ratchetStringToArrayBuffer(currentRatchet), 411 | pendingPreKey: pendingPreKey ? pendingPreKeyStringToArrayBuffer(pendingPreKey) : undefined, 412 | oldRatchetList: oldRatchetList.map(oldRatchetInfoStringToArrayBuffer), 413 | chains: newChains, 414 | } 415 | } 416 | 417 | export function sessionTypeArrayBufferToString(sess: SessionType): SessionType { 418 | const { indexInfo, registrationId, currentRatchet, pendingPreKey, oldRatchetList, chains } = sess 419 | const newChains: { [ephKeyString: string]: Chain } = {} 420 | for (const k of Object.keys(chains)) { 421 | newChains[k] = chainArrayBufferToString(chains[k]) 422 | } 423 | return { 424 | indexInfo: indexInfoArrayBufferToString(indexInfo), 425 | registrationId, 426 | currentRatchet: ratchetArrayBufferToString(currentRatchet), 427 | pendingPreKey: pendingPreKey ? pendingPreKeyArrayBufferToString(pendingPreKey) : undefined, 428 | oldRatchetList: oldRatchetList.map(oldRatchetInfoArrayBufferToString), 429 | chains: newChains, 430 | } 431 | } 432 | 433 | /* 434 | 435 | var Internal = Internal || {}; 436 | 437 | Internal.BaseKeyType = { 438 | OURS: 1, 439 | THEIRS: 2 440 | }; 441 | Internal.ChainType = { 442 | SENDING: 1, 443 | RECEIVING: 2 444 | }; 445 | 446 | 447 | 448 | var migrations = [ 449 | { 450 | version: 'v1', 451 | migrate: function migrateV1(data) { 452 | var sessions = data.sessions; 453 | var key; 454 | if (data.registrationId) { 455 | for (key in sessions) { 456 | if (!sessions[key].registrationId) { 457 | sessions[key].registrationId = data.registrationId; 458 | } 459 | } 460 | } else { 461 | for (key in sessions) { 462 | if (sessions[key].indexInfo.closed === -1) { 463 | // console.log('V1 session storage migration error: registrationId', 464 | // data.registrationId, 'for open session version', 465 | // data.version); 466 | } 467 | } 468 | } 469 | } 470 | } 471 | ]; 472 | 473 | function migrate(data) { 474 | var run = (data.version === undefined); 475 | for (var i=0; i < migrations.length; ++i) { 476 | if (run) { 477 | migrations[i].migrate(data); 478 | } else if (migrations[i].version === data.version) { 479 | run = true; 480 | } 481 | } 482 | if (!run) { 483 | throw new Error("Error migrating SessionRecord"); 484 | } 485 | } 486 | 487 | , 488 | 489 | , 490 | }();*/ 491 | -------------------------------------------------------------------------------- /src/__test__/session-cipher.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { SessionCipher, MessageType } from '../session-cipher' 4 | import { SessionBuilder } from '../session-builder' 5 | import { generateIdentity, generatePreKeyBundle, assertEqualUint8Arrays } from '../__test-utils__/utils' 6 | 7 | import { SignalProtocolStore } from './storage-type' 8 | import { SignalProtocolAddress } from '../signal-protocol-address' 9 | import { SessionRecord } from '../session-record' 10 | import { TestVectors } from './testvectors' 11 | import * as Internal from '../internal' 12 | import { KeyPairType } from '../types' 13 | import * as utils from '../helpers' 14 | import { 15 | PreKeyWhisperMessage, 16 | PushMessageContentCompatible as PushMessageContent, 17 | IncomingPushMessageSignal_Type, 18 | PushMessageContent_Flags, 19 | WhisperMessage, 20 | } from '@privacyresearch/libsignal-protocol-protobuf-ts' 21 | import { BaseKeyType } from '../session-types' 22 | 23 | const tv = TestVectors() 24 | 25 | const store = new SignalProtocolStore() 26 | const registrationId = 1337 27 | const address = new SignalProtocolAddress('foo', 1) 28 | const sessionCipher = new SessionCipher(store, address.toString()) 29 | 30 | const record = new SessionRecord(registrationId) 31 | const session = { 32 | registrationId: registrationId, 33 | currentRatchet: { 34 | rootKey: new ArrayBuffer(32), 35 | lastRemoteEphemeralKey: new ArrayBuffer(32), 36 | previousCounter: 0, 37 | }, 38 | indexInfo: { 39 | baseKey: new ArrayBuffer(32), 40 | baseKeyType: BaseKeyType.OURS, 41 | remoteIdentityKey: new ArrayBuffer(32), 42 | closed: -1, 43 | }, 44 | oldRatchetList: [], 45 | chains: {}, 46 | } 47 | record.updateSessionState(session) 48 | const prep = store.storeSession(address.toString(), record.serialize()) 49 | 50 | test('getRemoteRegistrationId, when an open record exists, returns a valid registrationId', async () => { 51 | await prep 52 | const value = await sessionCipher.getRemoteRegistrationId() 53 | expect(value).toBe(registrationId) 54 | }) 55 | 56 | test('getRemoteRegistrationId, when a record does not exist, returns undefined', async () => { 57 | await prep 58 | const sessionCipher = new SessionCipher(store, 'bar.1') 59 | const value = await sessionCipher.getRemoteRegistrationId() 60 | expect(value).toBeUndefined() 61 | }) 62 | 63 | test('hasOpenSession returns true', async () => { 64 | await prep 65 | const value = await sessionCipher.hasOpenSession() 66 | expect(value).toBeTruthy() 67 | }) 68 | 69 | it('hasOpenSession: no open session exists returns false', async () => { 70 | await prep 71 | const address = new SignalProtocolAddress('bar', 1) 72 | const sessionCipher = new SessionCipher(store, address.toString()) 73 | const record = new SessionRecord() 74 | await store.storeSession(address.toString(), record.serialize()) 75 | const value = await sessionCipher.hasOpenSession() 76 | expect(value).toBeFalsy() 77 | }) 78 | 79 | test('hasOpenSession: when there is no session returns false', async () => { 80 | await prep 81 | const address = new SignalProtocolAddress('baz', 1) 82 | const sessionCipher = new SessionCipher(store, address.toString()) 83 | const value = await sessionCipher.hasOpenSession() 84 | expect(value).toBeFalsy() 85 | }) 86 | //---------------------------------------------------------------------------------------------------- 87 | async function setupReceiveStep( 88 | store: SignalProtocolStore, 89 | data: { [k: string]: any }, 90 | privKeyQueue: ArrayBuffer[] 91 | ): Promise { 92 | if (data.newEphemeralKey !== undefined) { 93 | privKeyQueue.push(data.newEphemeralKey) 94 | } 95 | 96 | if (data.ourIdentityKey === undefined) { 97 | return Promise.resolve() 98 | } 99 | 100 | const keyPair = await Internal.crypto.createKeyPair(data.ourIdentityKey) 101 | store.put('identityKey', keyPair) 102 | const signedKeyPair = await Internal.crypto.createKeyPair(data.ourSignedPreKey) 103 | await store.storeSignedPreKey(data.signedPreKeyId, signedKeyPair) 104 | if (data.ourPreKey !== undefined) { 105 | const keyPair = await Internal.crypto.createKeyPair(data.ourPreKey) 106 | await store.storePreKey(data.preKeyId, keyPair) 107 | } 108 | } 109 | 110 | function getPaddedMessageLength(messageLength: number): number { 111 | const messageLengthWithTerminator = messageLength + 1 112 | let messagePartCount = Math.floor(messageLengthWithTerminator / 160) 113 | if (messageLengthWithTerminator % 160 !== 0) { 114 | messagePartCount++ 115 | } 116 | return messagePartCount * 160 117 | } 118 | 119 | function pad(plaintext: ArrayBuffer): ArrayBuffer { 120 | const paddedPlaintext = new Uint8Array(getPaddedMessageLength(plaintext.byteLength + 1) - 1) 121 | 122 | paddedPlaintext.set(new Uint8Array(plaintext)) 123 | paddedPlaintext[plaintext.byteLength] = 0x80 124 | return utils.uint8ArrayToArrayBuffer(paddedPlaintext) 125 | } 126 | 127 | function unpad(paddedPlaintext: Uint8Array): Uint8Array { 128 | const ppt = new Uint8Array(paddedPlaintext) 129 | 130 | for (let i = ppt.length - 1; i >= 0; i--) { 131 | if (ppt[i] == 0x80) { 132 | const plaintext = new Uint8Array(i) 133 | plaintext.set(ppt.subarray(0, i)) 134 | return plaintext 135 | } else if (ppt[i] !== 0x00) { 136 | throw new Error('Invalid padding') 137 | } 138 | } 139 | throw new Error('Invalid data: input empty or all 0x00s') 140 | } 141 | 142 | async function doReceiveStep( 143 | store: SignalProtocolStore, 144 | data: { [k: string]: any }, 145 | privKeyQueue: Array, 146 | address: SignalProtocolAddress 147 | ): Promise { 148 | await setupReceiveStep(store, data, privKeyQueue) 149 | const sessionCipher = new SessionCipher(store, address) 150 | 151 | try { 152 | let plaintext: Uint8Array 153 | if (data.type == IncomingPushMessageSignal_Type.CIPHERTEXT) { 154 | const dWS: Uint8Array = new Uint8Array(await sessionCipher.decryptWhisperMessage(data.message)) 155 | plaintext = await unpad(dWS) 156 | } else if (data.type == IncomingPushMessageSignal_Type.PREKEY_BUNDLE) { 157 | const dPKWS: Uint8Array = new Uint8Array(await sessionCipher.decryptPreKeyWhisperMessage(data.message)) 158 | plaintext = await unpad(dPKWS) 159 | } else { 160 | throw new Error('Unknown data type in test vector') 161 | } 162 | 163 | const content = PushMessageContent.decode(plaintext) 164 | if (data.expectTerminateSession) { 165 | if (content.flags == PushMessageContent_Flags.END_SESSION) { 166 | return true 167 | } else { 168 | return false 169 | } 170 | } 171 | 172 | return content.body === data.expectedSmsText 173 | } catch (e) { 174 | if (data.expectException) { 175 | return true 176 | } 177 | console.error(e) 178 | throw e 179 | } 180 | } 181 | 182 | async function setupSendStep( 183 | store: SignalProtocolStore, 184 | data: { [k: string]: any }, 185 | privKeyQueue: ArrayBuffer[] 186 | ): Promise { 187 | if (data.registrationId !== undefined) { 188 | store.put('registrationId', data.registrationId) 189 | } 190 | if (data.ourBaseKey !== undefined) { 191 | privKeyQueue.push(data.ourBaseKey) 192 | } 193 | if (data.ourEphemeralKey !== undefined) { 194 | privKeyQueue.push(data.ourEphemeralKey) 195 | } 196 | 197 | if (data.ourIdentityKey !== undefined) { 198 | try { 199 | const keyPair: KeyPairType = await Internal.crypto.createKeyPair(data.ourIdentityKey) 200 | store.put('identityKey', keyPair) 201 | } catch (e) { 202 | console.error({ e }) 203 | } 204 | } 205 | return Promise.resolve() 206 | } 207 | 208 | async function doSendStep( 209 | store: SignalProtocolStore, 210 | data: { [k: string]: any }, 211 | privKeyQueue: Array, 212 | address: SignalProtocolAddress 213 | ): Promise { 214 | await setupSendStep(store, data, privKeyQueue) 215 | try { 216 | if (data.getKeys !== undefined) { 217 | const deviceObject = { 218 | encodedNumber: address.toString(), 219 | identityKey: data.getKeys.identityKey, 220 | preKey: data.getKeys.devices[0].preKey, 221 | signedPreKey: data.getKeys.devices[0].signedPreKey, 222 | registrationId: data.getKeys.devices[0].registrationId, 223 | } 224 | const builder = new SessionBuilder(store, address) 225 | await builder.processPreKey(deviceObject) 226 | } 227 | 228 | const proto = PushMessageContent.fromJSON({}) 229 | if (data.endSession) { 230 | proto.flags = PushMessageContent_Flags.END_SESSION 231 | } else { 232 | proto.body = data.smsText 233 | } 234 | 235 | const sessionCipher = new SessionCipher(store, address) 236 | const pt = PushMessageContent.encode(proto).finish() 237 | 238 | if (data.endSession) { 239 | // console.log(`END SESSION PROTO`, { proto, pt }) 240 | } 241 | const msg = await sessionCipher.encrypt(pad(utils.uint8ArrayToArrayBuffer(pt))) 242 | 243 | const msgbody = new Uint8Array(utils.binaryStringToArrayBuffer(msg.body!.substring(1))!) 244 | // NOTE: equivalent protobuf objects can have different binary encodings and still be accepted by our 245 | // parsers to produce quivalent objects. Instead of testing binary identity of the entire 246 | // protobuf message, we parse it and check field-level identity. 247 | let res: boolean 248 | if (msg.type === 1) { 249 | res = utils.isEqual(data.expectedCiphertext, utils.binaryStringToArrayBuffer(msg.body || '')) 250 | } else { 251 | if (new Uint8Array(data.expectedCiphertext)[0] !== msg.body?.charCodeAt(0)) { 252 | throw new Error('Bad version byte') 253 | } 254 | // console.log({ 255 | // expectedCiphertext: data.expectedCiphertext, 256 | // msg: msgbody, 257 | // }) 258 | 259 | const ourpkwmsg = PreKeyWhisperMessage.decode(msgbody) 260 | const datapkwmsg = PreKeyWhisperMessage.decode(new Uint8Array(data.expectedCiphertext).slice(1)) 261 | 262 | assertEqualUint8Arrays(datapkwmsg.baseKey, ourpkwmsg.baseKey) 263 | assertEqualUint8Arrays(datapkwmsg.identityKey, ourpkwmsg.identityKey) 264 | expect(datapkwmsg.preKeyId).toStrictEqual(ourpkwmsg.preKeyId) 265 | expect(datapkwmsg.signedPreKeyId).toStrictEqual(ourpkwmsg.signedPreKeyId) 266 | 267 | const ourencrypted = WhisperMessage.decode(ourpkwmsg.message.slice(1, ourpkwmsg.message.length - 8)) 268 | const dataencrypted = WhisperMessage.decode(datapkwmsg.message.slice(1, datapkwmsg.message.length - 8)) 269 | 270 | expect(ourencrypted.counter).toBe(dataencrypted.counter) 271 | expect(ourencrypted.previousCounter).toBe(dataencrypted.previousCounter) 272 | assertEqualUint8Arrays(ourencrypted.ephemeralKey, dataencrypted.ephemeralKey) 273 | assertEqualUint8Arrays(ourencrypted.ciphertext, dataencrypted.ciphertext) 274 | 275 | const expected = PreKeyWhisperMessage.encode(datapkwmsg).finish() 276 | 277 | if ( 278 | !utils.isEqual( 279 | utils.uint8ArrayToArrayBuffer(expected), 280 | utils.binaryStringToArrayBuffer(msg.body.substring(1)) 281 | ) 282 | ) { 283 | throw new Error('Result does not match expected ciphertext') 284 | } 285 | 286 | res = true 287 | } 288 | if (data.endSession) { 289 | await sessionCipher.closeOpenSessionForDevice() 290 | return res 291 | } 292 | return res 293 | } catch (e) { 294 | console.error(e, { store }) 295 | throw e 296 | } 297 | } 298 | 299 | function getDescription(step: { [k: string]: any }): string { 300 | const direction = step[0] 301 | const data = step[1] 302 | if (direction === 'receiveMessage') { 303 | if (data.expectTerminateSession) { 304 | return 'receive end session message' 305 | } else if (data.type === 3) { 306 | return 'receive prekey message ' + data.expectedSmsText 307 | } else { 308 | return 'receive message ' + data.expectedSmsText 309 | } 310 | } else if (direction === 'sendMessage') { 311 | if (data.endSession) { 312 | return 'send end session message' 313 | } else if (data.ourIdentityKey) { 314 | return 'send prekey message ' + data.smsText 315 | } else { 316 | return 'send message ' + data.smsText 317 | } 318 | } 319 | return '' 320 | } 321 | 322 | tv.forEach(function (test) { 323 | describe(test.name, () => { 324 | const privKeyQueue: ArrayBuffer[] = [] 325 | const origCreateKeyPair = Internal.crypto.createKeyPair.bind(Internal.crypto) 326 | 327 | beforeAll(function () { 328 | // Shim createKeyPair to return predetermined keys from 329 | // privKeyQueue instead of random keys. 330 | Internal.crypto.createKeyPair = function (privKey) { 331 | if (privKey !== undefined) { 332 | return origCreateKeyPair(privKey) 333 | } 334 | if (privKeyQueue.length == 0) { 335 | throw new Error('Out of private keys') 336 | } else { 337 | const privKey = privKeyQueue.shift() 338 | return Internal.crypto.createKeyPair(privKey).then(function (keyPair) { 339 | if ( 340 | !privKey || 341 | utils.arrayBufferToString(keyPair.privKey) != utils.arrayBufferToString(privKey) 342 | ) 343 | throw new Error('Failed to rederive private key!') 344 | else return keyPair 345 | }) 346 | } 347 | } 348 | }) 349 | 350 | afterAll(function () { 351 | Internal.crypto.createKeyPair = origCreateKeyPair 352 | if (privKeyQueue.length != 0) { 353 | throw new Error('Leftover private keys') 354 | } 355 | }) 356 | 357 | const store = new SignalProtocolStore() 358 | const address = SignalProtocolAddress.fromString('SNOWDEN.1') 359 | test.vectors.forEach(function (step) { 360 | it(getDescription(step), async () => { 361 | let doStep: ( 362 | store: SignalProtocolStore, 363 | data: Record, 364 | q: ArrayBuffer[], 365 | address: SignalProtocolAddress 366 | ) => Promise 367 | 368 | if (step[0] === 'receiveMessage') { 369 | doStep = doReceiveStep 370 | } else if (step[0] === 'sendMessage') { 371 | doStep = doSendStep 372 | } else { 373 | throw new Error('Invalid test') 374 | } 375 | 376 | await expect(doStep(store, step[1], privKeyQueue, address)).resolves.toBeTruthy() //.then(assert).then(done, done) 377 | }) 378 | }) 379 | }) 380 | }) 381 | 382 | describe('key changes', function () { 383 | const ALICE_ADDRESS = new SignalProtocolAddress('+14151111111', 1) 384 | const BOB_ADDRESS = new SignalProtocolAddress('+14152222222', 1) 385 | const originalMessage = utils.binaryStringToArrayBuffer("L'homme est condamné à être libre") 386 | 387 | const aliceStore = new SignalProtocolStore() 388 | 389 | const bobStore = new SignalProtocolStore() 390 | const bobPreKeyId = 1337 391 | const bobSignedKeyId = 1 392 | 393 | const bobSessionCipher = new SessionCipher(bobStore, ALICE_ADDRESS) 394 | 395 | beforeAll(function (done) { 396 | Promise.all([aliceStore, bobStore].map(generateIdentity)) 397 | .then(function () { 398 | return generatePreKeyBundle(bobStore, bobPreKeyId, bobSignedKeyId) 399 | }) 400 | .then((preKeyBundle) => { 401 | const builder = new SessionBuilder(aliceStore, BOB_ADDRESS) 402 | return builder 403 | .processPreKey(preKeyBundle) 404 | .then(function () { 405 | const aliceSessionCipher = new SessionCipher(aliceStore, BOB_ADDRESS) 406 | return aliceSessionCipher.encrypt(originalMessage) 407 | }) 408 | .then(function (ciphertext) { 409 | return bobSessionCipher.decryptPreKeyWhisperMessage(ciphertext.body!, 'binary') 410 | }) 411 | .then(function () { 412 | done() 413 | }) 414 | .catch(done) 415 | }) 416 | }) 417 | 418 | describe("When bob's identity changes", function () { 419 | let messageFromBob: MessageType 420 | beforeAll(async () => { 421 | const ciphertext = await bobSessionCipher.encrypt(originalMessage) 422 | messageFromBob = ciphertext 423 | await generateIdentity(bobStore) 424 | const idK = bobStore.get('identityKey', undefined) as KeyPairType 425 | const pubK = idK.pubKey 426 | await aliceStore.saveIdentity(BOB_ADDRESS.toString(), pubK) 427 | }) 428 | 429 | test('alice cannot encrypt with the old session', async () => { 430 | const aliceSessionCipher = new SessionCipher(aliceStore, BOB_ADDRESS) 431 | await expect(async () => { 432 | await aliceSessionCipher.encrypt(originalMessage) 433 | }).rejects.toThrow('Identity key changed') 434 | }) 435 | 436 | test('alice cannot decrypt from the old session', async () => { 437 | const aliceSessionCipher = new SessionCipher(aliceStore, BOB_ADDRESS) 438 | await expect(async () => { 439 | await aliceSessionCipher.decryptWhisperMessage(messageFromBob.body, 'binary') 440 | }).rejects.toThrow('Identity key changed') 441 | }) 442 | }) 443 | }) 444 | -------------------------------------------------------------------------------- /src/session-cipher.ts: -------------------------------------------------------------------------------- 1 | import { StorageType, Direction } from './types' 2 | import { Chain, ChainType, SessionType } from './session-types' 3 | import { SignalProtocolAddress } from './signal-protocol-address' 4 | import { PreKeyWhisperMessage, WhisperMessage } from '@privacyresearch/libsignal-protocol-protobuf-ts' 5 | import * as base64 from 'base64-js' 6 | import * as util from './helpers' 7 | import * as Internal from './internal' 8 | 9 | import { SessionRecord } from './session-record' 10 | import { SessionLock } from './session-lock' 11 | import { SessionBuilder } from './session-builder' 12 | import { uint8ArrayToArrayBuffer } from './helpers' 13 | 14 | export interface MessageType { 15 | type: number 16 | body?: string 17 | registrationId?: number 18 | } 19 | export class SessionCipher { 20 | storage: StorageType 21 | remoteAddress: SignalProtocolAddress 22 | constructor(storage: StorageType, remoteAddress: SignalProtocolAddress | string) { 23 | this.storage = storage 24 | this.remoteAddress = 25 | typeof remoteAddress === 'string' ? SignalProtocolAddress.fromString(remoteAddress) : remoteAddress 26 | } 27 | async getRecord(encodedNumber: string): Promise { 28 | const serialized = await this.storage.loadSession(encodedNumber) 29 | if (serialized === undefined) { 30 | return undefined 31 | } 32 | return SessionRecord.deserialize(serialized) 33 | } 34 | 35 | encrypt(buffer: ArrayBuffer): Promise { 36 | return SessionLock.queueJobForNumber(this.remoteAddress.toString(), () => this.encryptJob(buffer)) 37 | } 38 | private encryptJob = async (buffer: ArrayBuffer) => { 39 | if (!(buffer instanceof ArrayBuffer)) { 40 | throw new Error('Expected buffer to be an ArrayBuffer') 41 | } 42 | 43 | const address = this.remoteAddress.toString() 44 | const msg = WhisperMessage.fromJSON({}) 45 | const [ourIdentityKey, myRegistrationId, record] = await this.loadKeysAndRecord(address) 46 | if (!record) { 47 | throw new Error('No record for ' + address) 48 | } 49 | if (!ourIdentityKey) { 50 | throw new Error(`cannot encrypt without identity key`) 51 | } 52 | // if (!myRegistrationId) { 53 | // throw new Error(`cannot encrypt without registration id`) 54 | // } 55 | 56 | const { session, chain } = await this.prepareChain(address, record, msg) 57 | 58 | const keys = await Internal.HKDF( 59 | chain.messageKeys[chain.chainKey.counter], 60 | new ArrayBuffer(32), 61 | 'WhisperMessageKeys' 62 | ) 63 | 64 | delete chain.messageKeys[chain.chainKey.counter] 65 | msg.counter = chain.chainKey.counter 66 | msg.previousCounter = session.currentRatchet.previousCounter 67 | 68 | const ciphertext = await Internal.crypto.encrypt(keys[0], buffer, keys[2].slice(0, 16)) 69 | msg.ciphertext = new Uint8Array(ciphertext) 70 | const encodedMsg = WhisperMessage.encode(msg).finish() 71 | 72 | const macInput = new Uint8Array(encodedMsg.byteLength + 33 * 2 + 1) 73 | macInput.set(new Uint8Array(ourIdentityKey.pubKey)) 74 | macInput.set(new Uint8Array(session.indexInfo.remoteIdentityKey), 33) 75 | macInput[33 * 2] = (3 << 4) | 3 76 | macInput.set(new Uint8Array(encodedMsg), 33 * 2 + 1) 77 | 78 | const mac = await Internal.crypto.sign(keys[1], macInput.buffer) 79 | 80 | const encodedMsgWithMAC = new Uint8Array(encodedMsg.byteLength + 9) 81 | encodedMsgWithMAC[0] = (3 << 4) | 3 82 | encodedMsgWithMAC.set(new Uint8Array(encodedMsg), 1) 83 | encodedMsgWithMAC.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1) 84 | 85 | const trusted = await this.storage.isTrustedIdentity( 86 | this.remoteAddress.getName(), 87 | session.indexInfo.remoteIdentityKey, 88 | Direction.SENDING 89 | ) 90 | if (!trusted) { 91 | throw new Error('Identity key changed') 92 | } 93 | 94 | this.storage.saveIdentity(this.remoteAddress.toString(), session.indexInfo.remoteIdentityKey) 95 | record.updateSessionState(session) 96 | await this.storage.storeSession(address, record.serialize()) 97 | 98 | if (session.pendingPreKey !== undefined) { 99 | const preKeyMsg = PreKeyWhisperMessage.fromJSON({}) 100 | preKeyMsg.identityKey = new Uint8Array(ourIdentityKey.pubKey) 101 | 102 | // TODO: for some test vectors there is no registration id. Why? 103 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 104 | preKeyMsg.registrationId = myRegistrationId! 105 | 106 | preKeyMsg.baseKey = new Uint8Array(session.pendingPreKey.baseKey) 107 | if (session.pendingPreKey.preKeyId) { 108 | preKeyMsg.preKeyId = session.pendingPreKey.preKeyId 109 | } 110 | preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId 111 | 112 | preKeyMsg.message = encodedMsgWithMAC 113 | const encodedPreKeyMsg = PreKeyWhisperMessage.encode(preKeyMsg).finish() 114 | const result = String.fromCharCode((3 << 4) | 3) + util.uint8ArrayToString(encodedPreKeyMsg) 115 | return { 116 | type: 3, 117 | body: result, 118 | registrationId: session.registrationId, 119 | } 120 | } else { 121 | return { 122 | type: 1, 123 | body: util.uint8ArrayToString(encodedMsgWithMAC), 124 | registrationId: session.registrationId, 125 | } 126 | } 127 | } 128 | 129 | private loadKeysAndRecord = (address: string) => { 130 | return Promise.all([ 131 | this.storage.getIdentityKeyPair(), 132 | this.storage.getLocalRegistrationId(), 133 | this.getRecord(address), 134 | ]) 135 | } 136 | 137 | private prepareChain = async (address: string, record: SessionRecord, msg: WhisperMessage) => { 138 | const session = record.getOpenSession() 139 | if (!session) { 140 | throw new Error('No session to encrypt message for ' + address) 141 | } 142 | if (!session.currentRatchet.ephemeralKeyPair) { 143 | throw new Error(`ratchet missing ephemeralKeyPair`) 144 | } 145 | 146 | msg.ephemeralKey = new Uint8Array(session.currentRatchet.ephemeralKeyPair.pubKey) 147 | const searchKey = base64.fromByteArray(msg.ephemeralKey) 148 | 149 | const chain = session.chains[searchKey] 150 | if (chain?.chainType === ChainType.RECEIVING) { 151 | throw new Error('Tried to encrypt on a receiving chain') 152 | } 153 | 154 | await this.fillMessageKeys(chain, chain.chainKey.counter + 1) 155 | return { session, chain } 156 | } 157 | 158 | private fillMessageKeys = async (chain: Chain, counter: number): Promise => { 159 | if (chain.chainKey.counter >= counter) { 160 | return Promise.resolve() // Already calculated 161 | } 162 | 163 | if (counter - chain.chainKey.counter > 2000) { 164 | throw new Error('Over 2000 messages into the future!') 165 | } 166 | 167 | if (chain.chainKey.key === undefined) { 168 | throw new Error('Got invalid request to extend chain after it was already closed') 169 | } 170 | 171 | const ckey = chain.chainKey.key 172 | if (!ckey) { 173 | throw new Error(`chain key is missing`) 174 | } 175 | 176 | // Compute KDF_CK as described in X3DH specification 177 | const byteArray = new Uint8Array(1) 178 | byteArray[0] = 1 179 | const mac = await Internal.crypto.sign(ckey, byteArray.buffer) 180 | byteArray[0] = 2 181 | const key = await Internal.crypto.sign(ckey, byteArray.buffer) 182 | 183 | chain.messageKeys[chain.chainKey.counter + 1] = mac 184 | chain.chainKey.key = key 185 | chain.chainKey.counter += 1 186 | await this.fillMessageKeys(chain, counter) 187 | } 188 | 189 | private async calculateRatchet(session: SessionType, remoteKey: ArrayBuffer, sending: boolean) { 190 | const ratchet = session.currentRatchet 191 | 192 | if (!ratchet.ephemeralKeyPair) { 193 | throw new Error(`currentRatchet has no ephemeral key. Cannot calculateRatchet.`) 194 | } 195 | const sharedSecret = await Internal.crypto.ECDHE(remoteKey, ratchet.ephemeralKeyPair.privKey) 196 | const masterKey = await Internal.HKDF(sharedSecret, ratchet.rootKey, 'WhisperRatchet') 197 | let ephemeralPublicKey 198 | if (sending) { 199 | ephemeralPublicKey = ratchet.ephemeralKeyPair.pubKey 200 | } else { 201 | ephemeralPublicKey = remoteKey 202 | } 203 | session.chains[base64.fromByteArray(new Uint8Array(ephemeralPublicKey))] = { 204 | messageKeys: {}, 205 | chainKey: { counter: -1, key: masterKey[1] }, 206 | chainType: sending ? ChainType.SENDING : ChainType.RECEIVING, 207 | } 208 | ratchet.rootKey = masterKey[0] 209 | } 210 | 211 | async decryptPreKeyWhisperMessage(buff: string | ArrayBuffer, encoding?: string): Promise { 212 | encoding = encoding || 'binary' 213 | if (encoding !== 'binary') { 214 | throw new Error(`unsupported encoding: ${encoding}`) 215 | } 216 | 217 | const buffer = typeof buff === 'string' ? util.binaryStringToArrayBuffer(buff) : buff 218 | const view = new Uint8Array(buffer) 219 | const version = view[0] 220 | const messageData = view.slice(1) 221 | 222 | if ((version & 0xf) > 3 || version >> 4 < 3) { 223 | // min version > 3 or max version < 3 224 | throw new Error('Incompatible version number on PreKeyWhisperMessage') 225 | } 226 | 227 | const address = this.remoteAddress.toString() 228 | const job = async () => { 229 | let record = await this.getRecord(address) 230 | const preKeyProto = PreKeyWhisperMessage.decode(messageData) 231 | if (!record) { 232 | if (preKeyProto.registrationId === undefined) { 233 | throw new Error('No registrationId') 234 | } 235 | record = new SessionRecord() // (preKeyProto.registrationId)??? 236 | } 237 | const builder = new SessionBuilder(this.storage, this.remoteAddress) 238 | 239 | // isTrustedIdentity is called within processV3, no need to call it here 240 | const preKeyId = await builder.processV3(record, preKeyProto) 241 | const session = record.getSessionByBaseKey(uint8ArrayToArrayBuffer(preKeyProto.baseKey)) 242 | if (!session) { 243 | throw new Error( 244 | `unable to find session for base key ${base64.fromByteArray(preKeyProto.baseKey)}, ${ 245 | preKeyProto.baseKey.byteLength 246 | }` 247 | ) 248 | } 249 | const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session) 250 | record.updateSessionState(session) 251 | await this.storage.storeSession(address, record.serialize()) 252 | if (preKeyId !== undefined && preKeyId !== null) { 253 | await this.storage.removePreKey(preKeyId) 254 | } 255 | return plaintext 256 | } 257 | 258 | return SessionLock.queueJobForNumber(address, job) 259 | } 260 | async decryptWithSessionList( 261 | buffer: ArrayBuffer, 262 | sessionList: SessionType[], 263 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 264 | errors: any[] 265 | ): Promise<{ plaintext: ArrayBuffer; session: SessionType }> { 266 | // Iterate recursively through the list, attempting to decrypt 267 | // using each one at a time. Stop and return the result if we get 268 | // a valid result 269 | if (sessionList.length === 0) { 270 | return Promise.reject(errors[0]) 271 | } 272 | 273 | const session = sessionList.pop() 274 | if (!session) { 275 | return Promise.reject(errors[0]) 276 | } 277 | try { 278 | const plaintext = await this.doDecryptWhisperMessage(buffer, session) 279 | 280 | return { plaintext: plaintext, session: session } 281 | } catch (e) { 282 | if ((e as Error).name === 'MessageCounterError') { 283 | return Promise.reject(e) 284 | } 285 | 286 | errors.push(e) 287 | return this.decryptWithSessionList(buffer, sessionList, errors) 288 | } 289 | } 290 | 291 | decryptWhisperMessage(buff: string | ArrayBuffer, encoding?: string): Promise { 292 | encoding = encoding || 'binary' 293 | if (encoding !== 'binary') { 294 | throw new Error(`unsupported encoding: ${encoding}`) 295 | } 296 | const buffer = typeof buff === 'string' ? util.binaryStringToArrayBuffer(buff) : buff 297 | const address = this.remoteAddress.toString() 298 | const job = async () => { 299 | const record = await this.getRecord(address) 300 | if (!record) { 301 | throw new Error('No record for device ' + address) 302 | } 303 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 304 | const errors: any[] = [] 305 | const result = await this.decryptWithSessionList(buffer, record.getSessions(), errors) 306 | if (result.session.indexInfo.baseKey !== record.getOpenSession()?.indexInfo.baseKey) { 307 | record.archiveCurrentState() 308 | record.promoteState(result.session) 309 | } 310 | 311 | const trusted = await this.storage.isTrustedIdentity( 312 | this.remoteAddress.getName(), 313 | result.session.indexInfo.remoteIdentityKey, 314 | Direction.RECEIVING 315 | ) 316 | if (!trusted) { 317 | throw new Error('Identity key changed') 318 | } 319 | 320 | await this.storage.saveIdentity(address, result.session.indexInfo.remoteIdentityKey) 321 | record.updateSessionState(result.session) 322 | await this.storage.storeSession(address, record.serialize()) 323 | 324 | return result.plaintext 325 | } 326 | return SessionLock.queueJobForNumber(address, job) 327 | } 328 | 329 | async doDecryptWhisperMessage(messageBytes: ArrayBuffer, session: SessionType): Promise { 330 | const version = new Uint8Array(messageBytes)[0] 331 | if ((version & 0xf) > 3 || version >> 4 < 3) { 332 | // min version > 3 or max version < 3 333 | throw new Error('Incompatible version number on WhisperMessage ' + version) 334 | } 335 | const messageProto = messageBytes.slice(1, messageBytes.byteLength - 8) 336 | const mac = messageBytes.slice(messageBytes.byteLength - 8, messageBytes.byteLength) 337 | 338 | const message = WhisperMessage.decode(new Uint8Array(messageProto)) 339 | const remoteEphemeralKey = uint8ArrayToArrayBuffer(message.ephemeralKey) 340 | 341 | if (session === undefined) { 342 | return Promise.reject( 343 | new Error('No session found to decrypt message from ' + this.remoteAddress.toString()) 344 | ) 345 | } 346 | if (session.indexInfo.closed != -1) { 347 | // console.log('decrypting message for closed session') 348 | } 349 | 350 | await this.maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter) 351 | 352 | const chain = session.chains[base64.fromByteArray(message.ephemeralKey)] 353 | if (!chain) { 354 | console.warn(`no chain found for key`, { key: base64.fromByteArray(message.ephemeralKey), session }) 355 | } 356 | if (chain?.chainType === ChainType.SENDING) { 357 | throw new Error('Tried to decrypt on a sending chain') 358 | } 359 | 360 | await this.fillMessageKeys(chain, message.counter) 361 | 362 | const messageKey = chain.messageKeys[message.counter] 363 | if (messageKey === undefined) { 364 | const e = new Error('Message key not found. The counter was repeated or the key was not filled.') 365 | e.name = 'MessageCounterError' 366 | throw e 367 | } 368 | delete chain.messageKeys[message.counter] 369 | const keys = await Internal.HKDF(messageKey, new ArrayBuffer(32), 'WhisperMessageKeys') 370 | 371 | const ourIdentityKey = await this.storage.getIdentityKeyPair() 372 | if (!ourIdentityKey) { 373 | throw new Error(`Our identity key is missing. Cannot decrypt.`) 374 | } 375 | 376 | const macInput = new Uint8Array(messageProto.byteLength + 33 * 2 + 1) 377 | macInput.set(new Uint8Array(session.indexInfo.remoteIdentityKey)) 378 | macInput.set(new Uint8Array(ourIdentityKey.pubKey), 33) 379 | macInput[33 * 2] = (3 << 4) | 3 380 | macInput.set(new Uint8Array(messageProto), 33 * 2 + 1) 381 | 382 | await Internal.verifyMAC(macInput.buffer, keys[1], mac, 8) 383 | 384 | const plaintext = await Internal.crypto.decrypt( 385 | keys[0], 386 | uint8ArrayToArrayBuffer(message.ciphertext), 387 | keys[2].slice(0, 16) 388 | ) 389 | 390 | delete session.pendingPreKey 391 | return plaintext 392 | } 393 | 394 | async maybeStepRatchet(session: SessionType, remoteKey: ArrayBuffer, previousCounter: number): Promise { 395 | const remoteKeyString = base64.fromByteArray(new Uint8Array(remoteKey)) 396 | if (session.chains[remoteKeyString] !== undefined) { 397 | return Promise.resolve() 398 | } 399 | 400 | const ratchet = session.currentRatchet 401 | if (!ratchet.ephemeralKeyPair) { 402 | throw new Error(`attempting to step reatchet without ephemeral key`) 403 | } 404 | const previousRatchet = session.chains[base64.fromByteArray(new Uint8Array(ratchet.lastRemoteEphemeralKey))] 405 | if (previousRatchet !== undefined) { 406 | await this.fillMessageKeys(previousRatchet, previousCounter).then(function () { 407 | delete previousRatchet.chainKey.key 408 | session.oldRatchetList[session.oldRatchetList.length] = { 409 | added: Date.now(), 410 | ephemeralKey: ratchet.lastRemoteEphemeralKey, 411 | } 412 | }) 413 | } 414 | 415 | await this.calculateRatchet(session, remoteKey, false) 416 | const previousRatchetKey = base64.fromByteArray(new Uint8Array(ratchet.ephemeralKeyPair.pubKey)) 417 | if (session.chains[previousRatchetKey] !== undefined) { 418 | ratchet.previousCounter = session.chains[previousRatchetKey].chainKey.counter 419 | delete session.chains[previousRatchetKey] 420 | } 421 | const keyPair = await Internal.crypto.createKeyPair() 422 | ratchet.ephemeralKeyPair = keyPair 423 | await this.calculateRatchet(session, remoteKey, true) 424 | ratchet.lastRemoteEphemeralKey = remoteKey 425 | } 426 | 427 | ///////////////////////////////////////// 428 | // session management and storage access 429 | getRemoteRegistrationId(): Promise { 430 | return SessionLock.queueJobForNumber(this.remoteAddress.toString(), async () => { 431 | const record = await this.getRecord(this.remoteAddress.toString()) 432 | if (record === undefined) { 433 | return undefined 434 | } 435 | const openSession = record.getOpenSession() 436 | if (openSession === undefined) { 437 | return undefined 438 | } 439 | return openSession.registrationId 440 | }) 441 | } 442 | 443 | hasOpenSession(): Promise { 444 | const job = async () => { 445 | const record = await this.getRecord(this.remoteAddress.toString()) 446 | if (record === undefined) { 447 | return false 448 | } 449 | return record.haveOpenSession() 450 | } 451 | return SessionLock.queueJobForNumber(this.remoteAddress.toString(), job) 452 | } 453 | closeOpenSessionForDevice(): Promise { 454 | const address = this.remoteAddress.toString() 455 | const job = async () => { 456 | const record = await this.getRecord(this.remoteAddress.toString()) 457 | if (record === undefined || record.getOpenSession() === undefined) { 458 | return 459 | } 460 | 461 | record.archiveCurrentState() 462 | return this.storage.storeSession(address, record.serialize()) 463 | } 464 | 465 | return SessionLock.queueJobForNumber(address, job) 466 | } 467 | deleteAllSessionsForDevice(): Promise { 468 | // Used in session reset scenarios, where we really need to delete 469 | const address = this.remoteAddress.toString() 470 | const job = async () => { 471 | const record = await this.getRecord(this.remoteAddress.toString()) 472 | if (record === undefined) { 473 | return 474 | } 475 | 476 | record.deleteAllSessions() 477 | return this.storage.storeSession(address, record.serialize()) 478 | } 479 | return SessionLock.queueJobForNumber(address, job) 480 | } 481 | } 482 | 483 | /* 484 | 485 | S 486 | 487 | libsignal.SessionCipher = function(storage, remoteAddress) { 488 | var cipher = new SessionCipher(storage, remoteAddress); 489 | 490 | // returns a Promise that resolves to a ciphertext object 491 | this.encrypt = cipher.encrypt.bind(cipher); 492 | 493 | // returns a Promise that inits a session if necessary and resolves 494 | // to a decrypted plaintext array buffer 495 | this.decryptPreKeyWhisperMessage = cipher.decryptPreKeyWhisperMessage.bind(cipher); 496 | 497 | // returns a Promise that resolves to decrypted plaintext array buffer 498 | this.decryptWhisperMessage = cipher.decryptWhisperMessage.bind(cipher); 499 | 500 | this.getRemoteRegistrationId = cipher.getRemoteRegistrationId.bind(cipher); 501 | this.hasOpenSession = cipher.hasOpenSession.bind(cipher); 502 | this.closeOpenSessionForDevice = cipher.closeOpenSessionForDevice.bind(cipher); 503 | this.deleteAllSessionsForDevice = cipher.deleteAllSessionsForDevice.bind(cipher); 504 | }; 505 | */ 506 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------