├── .eslintignore ├── .gitignore ├── docs ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ └── OpenSans-LightItalic-webfont.woff ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── lang-css.js │ │ ├── Apache-License-2.0.txt │ │ └── prettify.js ├── styles │ ├── prettify-jsdoc.css │ ├── prettify-tomorrow.css │ └── jsdoc-default.css ├── index.html ├── global.html ├── lib_lns.js.html ├── module-lns.html └── module-session-client.html ├── LT2mP2DrmGD82gFnH16ty8ZtP6f33czpA6XgQdnuTVeT5bNGyy3vnaUezzKq1rEYyq3cvb2GBZ5LjCC6uqDyKnbvFki9aAX.png ├── .eslintrc.js ├── .github └── FUNDING.yml ├── LICENSE ├── package.json ├── external ├── protos │ ├── SubProtocol.proto │ └── SignalService.proto └── mnemonic │ ├── index.js │ ├── mnemonic.js │ └── english.json ├── lib ├── lib.binary.js ├── lns.js ├── send.js ├── protobuf.js ├── attachments.js ├── open_groups.js ├── blake2b.js ├── recv.js ├── open_group_v2.js └── lib.loki_crypto.js ├── README.md └── sample.js /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode/ 3 | seed.txt 4 | lastHash.txt 5 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /LT2mP2DrmGD82gFnH16ty8ZtP6f33czpA6XgQdnuTVeT5bNGyy3vnaUezzKq1rEYyq3cvb2GBZ5LjCC6uqDyKnbvFki9aAX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hesiod-project/node-session-client/HEAD/LT2mP2DrmGD82gFnH16ty8ZtP6f33czpA6XgQdnuTVeT5bNGyy3vnaUezzKq1rEYyq3cvb2GBZ5LjCC6uqDyKnbvFki9aAX.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2020: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 11 12 | }, 13 | rules: { 14 | 'no-multi-spaces': 'off', 15 | 'spaced-comment': 'off', 16 | 'space-before-function-paren': ['error', {anonymous: 'never', named: 'never', asyncArrow: 'always'}], 17 | 'no-var': 'error', 18 | 'no-constant-condition': 'off', 19 | 'comma-dangle': 'off', 20 | 'object-curly-spacing': 'off', 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [hesiod-project] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hesiod-project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session-client", 3 | "version": "1.0.0", 4 | "description": "Simple Session client", 5 | "main": "sample.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "lint": "node_modules/.bin/eslint . --cache --ext .js", 11 | "lint-fix": "node_modules/.bin/eslint . --fix --ext .js", 12 | "lint-full": "node_modules/.bin/eslint . --ext .js", 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "jsdoc": "jsdoc -d docs lib/lns.js session-client.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/neuroscr/node-session-client.git" 19 | }, 20 | "keywords": [ 21 | "Session" 22 | ], 23 | "author": "Ryan Tharp", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/neuroscr/node-session-client/issues" 27 | }, 28 | "homepage": "https://github.com/neuroscr/node-session-client#readme", 29 | "dependencies": { 30 | "buffer-crc32": "^0.2.13", 31 | "bytebuffer": "^5.0.1", 32 | "form-data": "^3.0.0", 33 | "libsignal": "^2.0.1", 34 | "libsodium-wrappers": "^0.7.8", 35 | "libsodium-wrappers-sumo": "^0.7.10", 36 | "node-fetch": "^2.6.7", 37 | "protobufjs": "^6.10.1" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^7.6.0", 41 | "eslint-config-standard": "^14.1.1", 42 | "eslint-plugin-import": "^2.22.0", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-promise": "^4.2.1", 45 | "eslint-plugin-standard": "^4.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /external/protos/SubProtocol.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package signalservice; 18 | 19 | option java_package = "org.whispersystems.websocket.messages.protobuf"; 20 | 21 | message WebSocketRequestMessage { 22 | optional string verb = 1; 23 | optional string path = 2; 24 | optional bytes body = 3; 25 | repeated string headers = 5; 26 | optional uint64 id = 4; 27 | } 28 | 29 | message WebSocketResponseMessage { 30 | optional uint64 id = 1; 31 | optional uint32 status = 2; 32 | optional string message = 3; 33 | repeated string headers = 5; 34 | optional bytes body = 4; 35 | } 36 | 37 | message WebSocketMessage { 38 | enum Type { 39 | UNKNOWN = 0; 40 | REQUEST = 1; 41 | RESPONSE = 2; 42 | } 43 | 44 | optional Type type = 1; 45 | optional WebSocketRequestMessage request = 2; 46 | optional WebSocketResponseMessage response = 3; 47 | } 48 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 55 | 56 |
57 | 58 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /lib/lib.binary.js: -------------------------------------------------------------------------------- 1 | // move to a binary utility lib 2 | const concatUInt8Array = (...args) => { 3 | const totalLength = args.reduce((acc, current) => acc + current.length, 0) 4 | 5 | const concatted = new Uint8Array(totalLength) 6 | let currentIndex = 0 7 | args.forEach(arr => { 8 | concatted.set(arr, currentIndex) 9 | currentIndex += arr.length 10 | }) 11 | 12 | return concatted 13 | } 14 | 15 | /** 16 | * Take a string value with the given encoding and converts it to an `ArrayBuffer`. 17 | * @param value The string value. 18 | * @param encoding The encoding of the string value. 19 | */ 20 | function encode(value, encoding) { 21 | //return ByteBuffer.wrap(value, encoding).toArrayBuffer(); 22 | const buf = Buffer.from(value, encoding) 23 | const ab = new ArrayBuffer(buf.length) 24 | const view = new Uint8Array(ab) 25 | for (let i = 0; i < buf.length; ++i) { 26 | view[i] = buf[i] 27 | } 28 | return ab 29 | } 30 | 31 | /** 32 | * Take a buffer and convert it to a string with the given encoding. 33 | * @param buffer The buffer. 34 | * @param stringEncoding The encoding of the converted string value. 35 | */ 36 | function decode(buffer, stringEncoding) { 37 | const buf = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength) 38 | return buf.toString(stringEncoding) 39 | // [] or Uint8Array 40 | //console.log('typeof', typeof(buffer), buffer) 41 | //return ByteBuffer.wrap(buffer).toString(stringEncoding); 42 | } 43 | 44 | const fromUInt8ArrayToBase64 = d => decode(d, 'base64') 45 | 46 | function fromBase64ToUint8Array(base64Str) { 47 | const buf = Buffer.from(base64Str, 'base64') 48 | return new Uint8Array(buf.buffer) 49 | } 50 | 51 | const stringToArrayBuffer = str => { 52 | if (typeof str !== 'string') { 53 | throw new TypeError("'string' must be a string") 54 | } 55 | 56 | return encode(str, 'binary') 57 | } 58 | 59 | const stringToUint8Array = str => { 60 | if (!str) { 61 | return new Uint8Array() 62 | } 63 | 64 | return new Uint8Array(stringToArrayBuffer(str)) 65 | } 66 | 67 | // FIXME: 68 | function hexStringToUint8Array(hexString) { 69 | if (hexString.length % 2 !== 0) { 70 | throw new Error('Invalid hexString') 71 | } 72 | const arrayBuffer = new Uint8Array(hexString.length / 2) 73 | 74 | for (let i = 0; i < hexString.length; i += 2) { 75 | const byteValue = parseInt(hexString.substr(i, 2), 16) 76 | if (isNaN(byteValue)) { 77 | throw new Error('Invalid hexString') 78 | } 79 | arrayBuffer[i / 2] = byteValue 80 | } 81 | 82 | return arrayBuffer 83 | } 84 | 85 | module.exports = { 86 | concatUInt8Array, 87 | encode, 88 | decode, 89 | fromUInt8ArrayToBase64, 90 | fromBase64ToUint8Array, 91 | stringToArrayBuffer, // not really used externally 92 | stringToUint8Array, // not really used externally 93 | hexStringToUint8Array 94 | } 95 | -------------------------------------------------------------------------------- /external/mnemonic/index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const mnemonic = require('./mnemonic.js') 3 | //const curve = require('curve25519-n') 4 | const _sodium = require('libsodium-wrappers') 5 | 6 | const SEEDSIZE = 16 // gives 12 seed words 7 | 8 | // always return a promise 9 | async function wordsToKeyPair(words) { 10 | const f3 = words.substr(0, 3) 11 | if (f3 === 'V2:') { 12 | return wordsToKeyPairV2(words.substr(3)) 13 | } else 14 | if (f3 === 'V3:') { 15 | return wordsToKeyPairV3(words.substr(3)) 16 | } 17 | return wordsToKeyPairV3(words) 18 | } 19 | 20 | // words is a space separate string 21 | function wordsToKeyPairV2(words) { 22 | console.warn('Using deprecation version 2 format') 23 | // converting seed words to pubkey 24 | const seedHex32 = mnemonic.mn_decode(words) 25 | // double it 26 | const seedHex64 = seedHex32.concat(seedHex32).substring(0, 64) 27 | 28 | //const priv1 = curve.makeSecretKey(Buffer.from(seedHex64, 'hex')) 29 | const publicBuffer = Buffer.concat([Buffer.from('05', 'hex'), curve.derivePublicKey(priv1)]) 30 | 31 | return { 32 | privKey: priv1, 33 | pubKey: publicBuffer 34 | } 35 | } 36 | 37 | async function wordsToKeyPairV3(words) { 38 | // converting seed words to pubkey 39 | const seedHex32 = mnemonic.mn_decode(words) // string 40 | // prefix with 32 0s 41 | const seedHex64 = seedHex32.concat(['0'.repeat(32), seedHex32]).substring(0, 64) // string 42 | 43 | await _sodium.ready 44 | const sodium = _sodium 45 | try { 46 | // convert seed to ed keypair 47 | const ed25519KeyPair = sodium.crypto_sign_seed_keypair( 48 | Buffer.from(seedHex64, 'hex') // convert hex str into buffer 49 | ) 50 | // ed to curve pubkey 51 | const x25519PublicKey = sodium.crypto_sign_ed25519_pk_to_curve25519( 52 | ed25519KeyPair.publicKey 53 | ) 54 | // prepend 05 (version) 55 | const origPub = new Uint8Array(x25519PublicKey) 56 | const prependedX25519PublicKey = new Uint8Array(33) 57 | prependedX25519PublicKey.set(origPub, 1) 58 | prependedX25519PublicKey[0] = 5 59 | 60 | // ed to curve private 61 | const x25519SecretKey = sodium.crypto_sign_ed25519_sk_to_curve25519( 62 | ed25519KeyPair.privateKey 63 | ) 64 | 65 | return { 66 | // is this safe in node? 67 | privKey: Buffer.from(x25519SecretKey.buffer), 68 | pubKey: Buffer.from(prependedX25519PublicKey.buffer), 69 | ed25519KeyPair, 70 | } 71 | } catch (err) { 72 | return { 73 | err: err 74 | } 75 | } 76 | } 77 | 78 | // new random one... 79 | async function newKeypair() { 80 | const seedBuf = crypto.randomBytes(SEEDSIZE) 81 | const words = await mnemonic.mn_encode(seedBuf.toString('hex')) 82 | const keypair = await wordsToKeyPairV3(words) 83 | if (keypair.err) { 84 | console.error('mnemonic::::index::newKeypair - err', keypair.err) 85 | return false 86 | } 87 | return { 88 | keypair: keypair, 89 | words: words 90 | } 91 | } 92 | 93 | module.exports = { 94 | newKeypair, 95 | wordsToKeyPair, 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-session-client 2 | Implementation of Session protocol in node 3 | 4 | Supports 5 | - Session protocol support (Direct messaging) 6 | - Recovery Phrase (13 words) 7 | - Support for communicating with the Loki 10.x network 8 | - File server v2 9 | - Avatars 10 | - Attachments 11 | - Open groups v3 12 | - receiving blinded public/open messages 13 | - sending blinded public/open messages 14 | - delete blinded public/open messages 15 | - receiving blinded DMs (inbox) 16 | - display names 17 | 18 | Working on: 19 | - LNS 20 | - bugs / error codes 21 | - closed group support 22 | - relying on less 3rd party NPMs (for security reasons) 23 | - pure web version 24 | 25 | ## installing nodejs 26 | 27 | ### CentOS NodeJS installation: 28 | 29 | `curl -sL https://rpm.nodesource.com/setup_18.x | sudo bash -` 30 | 31 | ### Ubuntu/Debian NodeJS installation: 32 | 33 | `curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -` 34 | 35 | then 36 | 37 | `sudo apt-get install -y nodejs` 38 | 39 | ## clone repo 40 | 41 | You can clone the repo many ways. I will include how to do this from the command line: 42 | 43 | This makes a local copy of the repo via https 44 | 45 | `git clone https://github.com/hesiod-project/node-session-client` 46 | 47 | Be sure to be inside of the project repo for the next steps 48 | 49 | `cd node-session-client` 50 | 51 | ## install dependencies 52 | 53 | from inside the project root directory 54 | 55 | `npm i` 56 | 57 | ## Example Usage 58 | 59 | 1. set up library instance, be sure to adjust path in require if not in the project root. 60 | 61 | ```js 62 | const SessionClient = require('./session-client.js') 63 | 64 | // You'll want an instance per SessionID you want to receive messages for 65 | const client = new SessionClient() 66 | ``` 67 | 68 | 2. Set up identity and send a message 69 | 70 | To generate a new identity and save it to disk as `seed.txt`. please change `YOUR_SESSON_ID_GOES_HERE` to your Session ID 71 | 72 | ```js 73 | const fs = require('fs') 74 | client.loadIdentity({ 75 | seed: fs.existsSync('seed.txt') && fs.readFileSync('seed.txt').toString(), 76 | displayName: 'Sample Session Client', 77 | }).then(async () => { 78 | // output recovery phrase if making an identity 79 | console.log(client.identityOutput) 80 | 81 | const SessionID = "YOUR_SESSON_ID_GOES_HERE" 82 | client.send(SessionID, 'Hello').then(() => { 83 | console.debug('Sent "Hello" to', SessionID) 84 | }) 85 | }) 86 | ``` 87 | 88 | ## Detailed Example 89 | 90 | [Example](sample.js) 91 | 92 | ## Documentation 93 | 94 | [Auto-generated Detailed Documentation](https://hesiod-project.github.io/node-session-client/) 95 | 96 | 97 | # Support our work 98 | 99 | Development depends on your support 100 | LT2mP2DrmGD82gFnH16ty8ZtP6f33czpA6XgQdnuTVeT5bNGyy3vnaUezzKq1rEYyq3cvb2GBZ5LjCC6uqDyKnbvFki9aAX 101 | 102 | QR Code: 103 | ![oxen://LT2mP2DrmGD82gFnH16ty8ZtP6f33czpA6XgQdnuTVeT5bNGyy3vnaUezzKq1rEYyq3cvb2GBZ5LjCC6uqDyKnbvFki9aAX](LT2mP2DrmGD82gFnH16ty8ZtP6f33czpA6XgQdnuTVeT5bNGyy3vnaUezzKq1rEYyq3cvb2GBZ5LjCC6uqDyKnbvFki9aAX.png) 104 | -------------------------------------------------------------------------------- /docs/global.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Global 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Global

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |

Members

98 | 99 | 100 | 101 |

(constant) FILESERVERV2_URL

102 | 103 | 104 | 105 | 106 |
107 | Default home server URL 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
Default Value:
142 |
    143 |
  • http://filev2.getsession.org
  • 144 |
145 | 146 | 147 | 148 |
Source:
149 |
152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |
175 | 176 |
177 | 178 | 179 | 180 | 181 |
182 | 183 | 186 | 187 |
188 | 189 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /sample.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') // for loading state 2 | // may need to adjust path until npm version is available 3 | const SessionClient = require('./session-client.js') 4 | 5 | // create an instance 6 | // You'll want an instance per SessionID you want to receive messages for 7 | const client = new SessionClient() 8 | 9 | // load place in inbox if available 10 | if (fs.existsSync('lastHash.txt')) { 11 | client.lastHash = fs.readFileSync('lastHash.txt').toString() 12 | } 13 | 14 | // load an SessionID into client and set some options 15 | client.loadIdentity({ 16 | // load recovery phrase if available 17 | seed: fs.existsSync('seed.txt') && fs.readFileSync('seed.txt').toString(), 18 | displayName: 'Sample Session Client', 19 | // path to local file 20 | //avatarFile: 'avatar.png', 21 | }).then(async () => { 22 | // output recovery phrase if making an identity 23 | console.log(client.identityOutput) 24 | 25 | // persist place in inbox incase we restart 26 | client.on('updateLastHash', hash => { 27 | fs.writeFileSync('lastHash.txt', hash) 28 | }) 29 | 30 | const openGroupV2URL = 'http://open2.hesiod.network/build_a_bot?public_key=58dc124cc38e4d03449037e9a4a86a2e5c2a648938eb824a5cdf3b6a80fab07d' 31 | const ogv2Handle = await client.joinOpenGroupV2(openGroupV2URL) 32 | 33 | // handle incoming messages 34 | client.on('messages', msgs => { 35 | msgs.forEach(async msg => { 36 | if (msg.room) { 37 | console.log(`New message, In group: ${msg.id} in ${msg.room}`) 38 | } else { 39 | console.log('New message, Private') 40 | } 41 | console.log(`From: ${msg.profile && msg.profile.displayName} (${msg.source})`) 42 | console.log(msg.body) 43 | 44 | // Download their avatar 45 | // change 0 to 1 if you want to download avatars from users that you receive 46 | if (0 && msg.profile && msg.profile.profilePicture) { 47 | const avatarBuf = await client.decodeAvatar(msg.profile.profilePicture, msg.profileKey) 48 | // write it to disk 49 | fs.writeFileSync(msg.source + '.avatar', avatarBuf) 50 | } 51 | 52 | // Attachment processing example 53 | // change 0 to 1 if you want to download (the first) attachment sent to you 54 | if (0) { 55 | if (msg.attachments.length) { 56 | const attachments = await client.getAttachments(msg) 57 | //console.log('attachment', attachments[0]) 58 | if (attachments[0]) { // if no errors 59 | fs.writeFileSync(msg.source + '.attachment', attachments[0]) 60 | } 61 | } 62 | } 63 | /* 64 | // Open group invitation example 65 | if (msg.groupInvitation) { 66 | console.log('got invite to channel', msg.groupInvitation.channelId) 67 | } 68 | */ 69 | }) 70 | }) 71 | // the await here allows send to reuse the cache it builds 72 | await client.open() 73 | 74 | // TODO: replace with your SessionID 75 | const SessionID = '' 76 | 77 | // LNS example 78 | // 79 | //const lnsUtils = require('./lib/lns.js') 80 | //const pubkey = await lnsUtils.getNameFast('root') 81 | //console.log('sid for root', pubkey) 82 | 83 | // Send message example 84 | // 85 | // need an image 86 | //const attachment = await client.makeImageAttachment(fs.readFileSync('/Users/user2/Pictures/1587699732-0s.png')) 87 | // 88 | // change 0 to 1 if you want to send a message to SessionID 89 | if (0) { 90 | client.send(SessionID, 'Hello', { 91 | // attachments: [attachment] 92 | }).then(() => { 93 | console.debug('Sent "Hello" to', SessionID) 94 | }) 95 | } 96 | // Open group invite example 97 | // change 0 to 1 if you want to send an open group invite to SessionID 98 | if (0) { 99 | client.sendOpenGroupInvite(SessionID, 'Bob\'s server', '') 100 | } 101 | 102 | // change 0 to 1 if you want to send a message to an open group 103 | if (0 && ogv2Handle) { 104 | const messageId = await client.sendOpenGroupV2Message(ogv2Handle, 'Hello World SOGSv2 from node-session-client') 105 | // change 0 to 1 if you want to delete that message you sent to an open group 106 | if (0) { 107 | await client.deleteOpenGroupV2Message(ogv2Handle, messageId) 108 | } 109 | } 110 | }) 111 | -------------------------------------------------------------------------------- /lib/lns.js: -------------------------------------------------------------------------------- 1 | const blake2bUtils = require('./blake2b.js') 2 | const lib = require('./lib.js') 3 | const _sodium = require('libsodium-wrappers') 4 | 5 | /** 6 | * Loki Name Service Utilities 7 | * @module lns 8 | * @exports {object} exports.getNameSafe 9 | * @exports {object} exports.getNameFast 10 | * @author Ryan Tharp 11 | * @license ISC 12 | */ 13 | 14 | async function getSodium() { 15 | await _sodium.ready 16 | return _sodium 17 | } 18 | 19 | // good candiate of a primitive 20 | /** 21 | * Lookup LNS name against three snode and get agreement 22 | * @param {String} lnsName what name to look up 23 | * @return {Promise} pubkey (SessionID) it points to 24 | */ 25 | async function getNameSafe(lnsName) { 26 | const requests = [...Array(3).keys()] 27 | const list = await Promise.all(requests.map(idx => getNameFast(lnsName))) 28 | if (list.every(v => v === list[0])) { 29 | return list[0] 30 | } 31 | } 32 | 33 | /** 34 | * Lookup LNS name against one snode 35 | * @param {String} lnsName what name to look up 36 | * @return {Promise} pubkey (SessionID) it points to 37 | */ 38 | async function getNameFast(lnsName) { 39 | const nameBuf = Buffer.from(lnsName) 40 | const uArr = blake2bUtils.blake2b(nameBuf, undefined, 32) 41 | const hash64 = Buffer.from(uArr).toString('base64') 42 | const snodeUrl = await lib.getRandomSnode() 43 | console.log('asking', snodeUrl, 'about', lnsName) 44 | const res = await lib.jsonrpc(snodeUrl, 'get_lns_mapping', { 45 | name_hash: hash64 46 | }) 47 | console.log('decoding', lnsName, 'response from', snodeUrl) 48 | // FIXME: handle network failures better... 49 | /* 50 | [ 51 | { 52 | backup_owner: '', 53 | encrypted_value: '61b77686bbb8bed9074386598d6e18e0a9cca8098c1dd4b643a643cabfbf133ef9f0cdd01adf252bf18eba378f6de003d8', 54 | entry_index: 0, 55 | name_hash: 'CFm/zhpmu+SVenlQLPED6xzTja5L3ncc1KLw/+ewrUk=', 56 | owner: 'LBtSHQi85YEGRj1y87dHYRfEQPFwHGCRv3ceqesuSo3PKXW1LhHcFLCUSMHJ9hdRejcJRiQHX1WzdWsdBRRGJzA8QBMY27J', 57 | prev_txid: '', 58 | register_height: 497549, 59 | txid: 'bd1b9ca44ae541b277cdf62811012823911365c28fe978f38c39115a2b747e5a', 60 | type: 0, 61 | update_height: 497549 62 | } 63 | ] 64 | */ 65 | //console.log('res', res.result.entries) 66 | if (!res || !res.result || !res.result.entries) { 67 | console.warn('lib:::lns::getName - Error retrieving', res) 68 | return 69 | } 70 | if (res.result.entries.length !== 1) { 71 | console.warn('lib:::lns::getName - Too many entries', res.result.entries) 72 | return 73 | } 74 | const obj = res.result.entries[0] 75 | const sodium = await getSodium() 76 | // salt is all 0s (16 bytes) 77 | const salt = new Uint8Array(sodium.crypto_pwhash_SALTBYTES) 78 | //console.log(lnsName, 'salt', salt) 79 | 80 | // these are different 81 | //console.log(lnsName, 'encrypted_value', obj.encrypted_value) 82 | 83 | //const cipherTextBuf = Buffer.from(obj.encrypted_value, 'hex') 84 | // make sure it's uint8array 85 | //const cipherText = new Uint8Array(cipherTextBuf.buffer, cipherTextBuf.byteOffset, cipherTextBuf.byteLength); 86 | const cipherText = sodium.from_hex(obj.encrypted_value) 87 | //console.log(lnsName, 'cipherText', cipherText) // 49 bytes... 88 | // try to decrypt 89 | try { 90 | const key = sodium.crypto_pwhash( 91 | sodium.crypto_secretbox_KEYBYTES, 92 | lnsName, // key 93 | salt, 94 | sodium.crypto_pwhash_OPSLIMIT_MODERATE, 95 | sodium.crypto_pwhash_MEMLIMIT_MODERATE, 96 | sodium.crypto_pwhash_ALG_ARGON2ID13 97 | ) 98 | 99 | // nonce should be all 0s (24 bytes) 100 | const nonce = new Uint8Array(sodium.crypto_secretbox_NONCEBYTES) 101 | //console.log(lnsName, 'nonce', nonce) 102 | 103 | /* 104 | const nonce = nonceAndCipherText.slice(0, sodium.crypto_secretbox_NONCEBYTES) 105 | const cipherText = nonceAndCipherText.slice(sodium.crypto_secretbox_NONCEBYTES) 106 | */ 107 | // this is uint8 108 | const decryptedVal = sodium.crypto_secretbox_open_easy(cipherText, nonce, key) 109 | // convert back to hex (it includes the 05 prefix since that's usually put into the LNS record) 110 | console.log('Decoded', lnsName, 'from', snodeUrl) 111 | return Buffer.from(decryptedVal).toString('hex') 112 | // 053b6b764388cd6c4d38ae0b3e7492a8ecf0076e270c013bb5693d973045f45254 will be a common response 113 | // means they haven't set their session id yet... 114 | } catch (err) { 115 | console.error('lib:::lns::getName - decryption err', err) 116 | } 117 | } 118 | 119 | module.exports = { 120 | getNameSafe: getNameSafe, 121 | getNameFast: getNameFast, 122 | } 123 | -------------------------------------------------------------------------------- /lib/send.js: -------------------------------------------------------------------------------- 1 | const protobuf = require('./protobuf.js') 2 | const _sodium = require('libsodium-wrappers') // maybe put in session-client? 3 | 4 | async function send(toPubkey, sourceKeypair, body, lib, options = {}) { 5 | await _sodium.ready 6 | const sodium = _sodium 7 | if (!sourceKeypair.ed25519KeyPair) { 8 | console.error('sourceKeypair does not have an ed25519 to send messages with') 9 | return 10 | } 11 | 12 | // Constants.TTL_DEFAULT.REGULAR_MESSAGE 13 | const ttl = 2 * 86400 * 1000 // in ms 14 | const timestamp = Date.now() 15 | 16 | const swarmPromise1 = lib.getSwarmsnodeUrl(toPubkey, sourceKeypair) 17 | const swarmPromise2 = lib.getSwarmsnodeUrl(toPubkey, sourceKeypair) 18 | const swarmPromise3 = lib.getSwarmsnodeUrl(toPubkey, sourceKeypair) 19 | 20 | // we need to cipher something... 21 | // device is pubkey... (cast?) 22 | // pt buf and encryption(fb) comes from message... 23 | // put cipherText into content... 24 | 25 | // MessageSender.ts 26 | //console.log('sending to', toPubkey) 27 | const destinationBuf = Buffer.from(toPubkey, 'hex').subarray(1) 28 | 29 | //console.log('send::send - options', options) 30 | 31 | const plaintext = protobuf.encodeContentMessage(body, timestamp, options) 32 | const verificationData = Buffer.concat([ 33 | plaintext, 34 | sourceKeypair.ed25519KeyPair.publicKey, 35 | destinationBuf 36 | ]) 37 | let signature 38 | try { 39 | signature = sodium.crypto_sign_detached( 40 | verificationData, 41 | sourceKeypair.ed25519KeyPair.privateKey 42 | ) 43 | } catch (e) { 44 | console.error('send failed', e) 45 | return 46 | } 47 | const plaintextWithMetadata = Buffer.concat([ 48 | plaintext, 49 | sourceKeypair.ed25519KeyPair.publicKey, 50 | signature 51 | ]) 52 | const content = sodium.crypto_box_seal( 53 | plaintextWithMetadata, 54 | destinationBuf 55 | ) 56 | if (!content) { 57 | console.error('send failed, could not encrypt') 58 | return 59 | } 60 | 61 | /* 62 | const content = fallbackUtils.fallbackEncrypt( 63 | sourceKeypair.privKey, toPubkey, protobuf.padPlainTextBuffer(contentBuf) 64 | ) 65 | */ 66 | // build an envelope 67 | const rawEnv = { 68 | // FALLBACK_MESSAGE 69 | //type: 101, 70 | // UNIDENTIFIED 71 | type: 6, 72 | source: sourceKeypair.pubKey.toString('hex'), 73 | sourceDevice: 1, 74 | timestamp: timestamp, 75 | // looking for a uint8array 76 | content: content 77 | } 78 | //console.log('env', rawEnv) 79 | const errMsg2 = protobuf.Envelope.verify(rawEnv) 80 | if (errMsg2) console.error('rawEnv verification', errMsg2) 81 | const envWrapper = protobuf.Envelope.create(rawEnv) 82 | const envBuf = protobuf.Envelope.encode(envWrapper).finish() 83 | 84 | //console.log('test env', envBuf) 85 | 86 | // MessageSender.ts 87 | const rawWsr = { 88 | id: 0, 89 | body: envBuf, 90 | verb: 'PUT', 91 | path: '/api/v1/message' 92 | } 93 | const errMsg3 = protobuf.WebSocketRequestMessage.verify(rawWsr) 94 | if (errMsg3) console.error('rawWsr verification', errMsg3) 95 | const wsrWrapper = protobuf.WebSocketRequestMessage.create(rawWsr) 96 | 97 | const rawWs = { 98 | // SignalService.WebSocketMessage.Type.REQUEST 99 | type: 1, 100 | request: wsrWrapper 101 | } 102 | const errMsg4 = protobuf.WebSocketMessage.verify(rawWs) 103 | if (errMsg4) console.error('rawWs verification', errMsg4) 104 | const wsWrapper = protobuf.WebSocketMessage.create(rawWs) 105 | const wsBuf = protobuf.WebSocketMessage.encode(wsWrapper).finish() 106 | 107 | // convert data to base64... 108 | const data64 = wsBuf.toString('base64') 109 | 110 | //const nonce = await pow.calcPoW(timestamp, ttl, toPubkey, data64, difficulty) 111 | //console.log('nonce', nonce, 'data64', data64) 112 | 113 | // loki_message.js 114 | const storeParams = { 115 | ttl: ttl.toString(), 116 | timestamp: timestamp, // .toString() 117 | data: data64 118 | } 119 | const swarmUrl = await swarmPromise1 // make sure swarmUrl is ready 120 | //console.log('storeParams', storeParams) 121 | const storeData = await lib.pubKeyAsk(swarmUrl, 'store', toPubkey, sourceKeypair, storeParams) 122 | 123 | const swarmUrl2 = await swarmPromise2 // make sure swarmUrl is ready 124 | await lib.pubKeyAsk(swarmUrl2, 'store', toPubkey, sourceKeypair, storeParams) 125 | const swarmUrl3 = await swarmPromise3 // make sure swarmUrl is ready 126 | await lib.pubKeyAsk(swarmUrl3, 'store', toPubkey, sourceKeypair, storeParams) 127 | 128 | if (!storeData || !storeData.swarm) { 129 | console.debug('lib::send - unexpected result', storeData) 130 | return false 131 | } 132 | // communicate swarm back to lib somehow or lib should handle this directly? 133 | // probably directly 134 | // oxen-storage-server isn't going to revert to older versions 135 | /* 136 | // object, { difficulty: 1 } 137 | if (!storeData || storeData.difficulty !== 1) { 138 | if (storeData && storeData.snodes) { 139 | // re-org? 140 | } 141 | // this probably the reorg? maybe not getting this every startup 142 | // reading docs this is normal 143 | if (storeData && storeData.swarm) { 144 | // storeData.swarm[key] = { hash, signature, t: timestamp in ms } 145 | // communicate this back to lib somehow or lib should handle this directly 146 | } else { 147 | console.debug('lib::send - unexpected result', storeData) 148 | } 149 | // used to be able to inform indicate we need to change POW difficulty 150 | return false 151 | } 152 | */ 153 | return true 154 | } 155 | 156 | module.exports = { 157 | send 158 | } 159 | -------------------------------------------------------------------------------- /lib/protobuf.js: -------------------------------------------------------------------------------- 1 | const protobuf = require('protobufjs') 2 | const path = require('path') 3 | const crypto = require('crypto') 4 | 5 | const protoPath = path.join(__dirname, '../external/protos/') 6 | 7 | protobuf.load(protoPath + 'SubProtocol.proto', function(err, protoRoot) { 8 | if (err) console.error('proto err', err) 9 | module.exports.WebSocketMessage = protoRoot.lookupType('WebSocketMessage') 10 | module.exports.WebSocketRequestMessage = protoRoot.lookupType('WebSocketRequestMessage') 11 | }) 12 | protobuf.load(protoPath + 'SignalService.proto', function(err, signalRoot) { 13 | if (err) console.error('proto err', err) 14 | module.exports.Envelope = signalRoot.lookupType('Envelope') 15 | module.exports.Content = signalRoot.lookupType('Content') 16 | module.exports.DataMessage = signalRoot.lookupType('DataMessage') 17 | module.exports.AttachmentPointer = signalRoot.lookupType('AttachmentPointer') 18 | module.exports.LokiProfile = signalRoot.lookupType('LokiProfile') 19 | module.exports.GroupInvitation = signalRoot.lookupType('GroupInvitation') 20 | }) 21 | 22 | function unpad(paddedData, id) { 23 | //console.log('unpad', paddedData) 24 | const paddedPlaintext = new Uint8Array(paddedData) 25 | //console.log('unpad last char is', paddedPlaintext[paddedPlaintext.length - 1]) 26 | for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) { 27 | if (paddedPlaintext[i] === 0x80) { 28 | const plaintext = new Uint8Array(i) 29 | plaintext.set(paddedPlaintext.subarray(0, i)) 30 | return plaintext.buffer 31 | } else if (paddedPlaintext[i] !== 0x00) { 32 | //throw new Error('Invalid padding') 33 | //console.log('No padding on', id) 34 | return paddedData 35 | } 36 | } 37 | 38 | throw new Error('Invalid padding') 39 | } 40 | 41 | function getPaddedMessageLength(originalLength) { 42 | const messageLengthWithTerminator = originalLength + 1 43 | let messagePartCount = Math.floor(messageLengthWithTerminator / 160) 44 | 45 | if (messageLengthWithTerminator % 160 !== 0) { 46 | messagePartCount += 1 47 | } 48 | 49 | return messagePartCount * 160 50 | } 51 | 52 | function padPlainTextBuffer(messageBuffer) { 53 | const plaintext = new Uint8Array( 54 | getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 55 | ) 56 | plaintext.set(new Uint8Array(messageBuffer)) 57 | plaintext[messageBuffer.byteLength] = 0x80 58 | 59 | return plaintext 60 | } 61 | 62 | function encodeContentMessage(text, ts, options) { 63 | const rawDM = { 64 | body: text, 65 | timestamp: ts, 66 | } 67 | if (options.attachments) { 68 | console.log('protobuf::encodeContentMessage - setting attachments', options.attachments.length) 69 | rawDM.attachments = options.attachments 70 | } 71 | if (options && (options.displayName || options.avatar)) { 72 | const profile = {} 73 | if (options.displayName) { 74 | profile.displayName = options.displayName 75 | } 76 | if (options.avatar) { 77 | // this is the avatarPointer, a URL on the file server... 78 | // used to be profile.avatar 79 | 80 | // this is deprecated 230120 81 | profile.profilePicture = options.avatar.url 82 | // this is the current method 230120 83 | profile.avatar = options.avatar.url 84 | 85 | const b = options.avatar.profileKeyBuf 86 | // convert buffer into Uint8Array 87 | const pk8 = new Uint8Array(b.buffer, b.byteOffset, b.byteLength) 88 | 89 | rawDM.profileKey = pk8 90 | } 91 | rawDM.profile = profile 92 | } 93 | if (options.groupInvitation) { 94 | // yea we don't need to create a protobuf for these sub-structures 95 | rawDM.groupInvitation = options.groupInvitation 96 | } 97 | if (options.flags) { 98 | rawDM.flags = options.flags 99 | } 100 | if (options.nullMessage) { 101 | const buffer = crypto.randomBytes(1) // random int between 1 and 512 102 | const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1 103 | console.log('protobuf::encodeContentMessage - paddingLength', paddingLength) 104 | rawDM.nullMessage = { 105 | padding: crypto.randomBytes(paddingLength) 106 | } 107 | // may need to tweak TTL for push notes on the recving end 108 | // Constants.TTL_DEFAULT.SESSION_ESTABLISHED 109 | // ttl = (2 * 86400) - (1 * 3600) 110 | 111 | // also maybe encrypted different... 112 | } 113 | if (rawDM.body === undefined) rawDM.body = '' 114 | const errMsg5 = module.exports.DataMessage.verify(rawDM) 115 | if (errMsg5) console.error('protobuf::encodeContentMessage - rawDM verification', errMsg5) 116 | //console.log('rawDM', rawDM) 117 | const dmWrapper = module.exports.DataMessage.create(rawDM) 118 | const rawContent = { 119 | dataMessage: dmWrapper 120 | } 121 | const errMsg = module.exports.Content.verify(rawContent) 122 | if (errMsg) console.error('protobuf::encodeContentMessage - rawContent verification', errMsg) 123 | const contentWrapper = module.exports.Content.create(rawContent) 124 | const contentBuf = module.exports.Content.encode(contentWrapper).finish() 125 | // padPlainTextBuffer returns Uint8Array 126 | return padPlainTextBuffer(contentBuf) 127 | //console.log('plaintextBuf', plaintextBuf, 'plaintextBuf', plaintextBuf.toString()) 128 | } 129 | 130 | // plaintext is output from sodium 131 | // maybe uint8_array 132 | // seems to like node Buffer tho 133 | function decodeContentMessage(plaintext, id) { 134 | // might need to unpad plaintext 135 | try { 136 | const unpaddedPlaintext = unpad(plaintext, id) 137 | const content = module.exports.Content.decode(new Uint8Array(unpaddedPlaintext)) 138 | return content 139 | } catch (e) { 140 | console.log('protobuf::decodeContentMessage - decoding failure on', id, e) 141 | } 142 | } 143 | 144 | module.exports = { 145 | padPlainTextBuffer, 146 | encodeContentMessage, 147 | decodeContentMessage, 148 | } 149 | -------------------------------------------------------------------------------- /lib/attachments.js: -------------------------------------------------------------------------------- 1 | const urlparser = require('url') 2 | const crypto = require('crypto') 3 | const fs = require('fs') 4 | const lib = require('./lib.js') 5 | // eslint-disable-next-line camelcase 6 | const loki_crypto = require('./lib.loki_crypto.js') 7 | 8 | // https://github.com/oxen-io/session-desktop/blob/clearnet/ts/session/apis/file_server_api/FileServerApi.ts 9 | // upload to file server v2 or open group server v2 10 | async function uploadFile(baseUrl, serverPubKey, data, token = false) { 11 | const buf = Buffer.from(data) 12 | const postBody = JSON.stringify({ 13 | file: buf.toString('base64'), 14 | }) 15 | let headers = {} 16 | 17 | // only needed for rooms? 18 | if (token) { 19 | headers = { 20 | Authorization: 'Bearer ' + token, 21 | } 22 | } 23 | let fileRes 24 | try { 25 | fileRes = await lib.lsrpc(baseUrl, '', serverPubKey, 'files', 'POST', postBody, headers) 26 | } catch (e) { 27 | console.error('attachments::uploadFile - err', e) 28 | } 29 | if (!fileRes || fileRes.status_code !== 200) { 30 | console.log('attachments::uploadFile - unknown result', fileRes) 31 | return false 32 | } 33 | return fileRes.result 34 | } 35 | 36 | // https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml 37 | async function uploadBufferV2(buf) { 38 | const ep = FILESERVERV2_URL + '/file' 39 | const method = 'POST' 40 | const res = await lib.jsonAsk(ep, { 41 | method, 42 | headers: { 43 | 'Content-length': buf.byteLength 44 | }, 45 | body: buf 46 | }) 47 | //console.log('res', res) 48 | return res?.id 49 | } 50 | 51 | async function uploadFileV2(path) { 52 | const stats = fs.statSync(path) 53 | const readStream = fs.createReadStream(path) 54 | 55 | const ep = FILESERVERV2_URL + '/file' 56 | const method = 'POST' 57 | const res = await lib.jsonAsk(ep, { 58 | method, 59 | headers: { 60 | 'Content-length': stats.size 61 | }, 62 | body: readStream 63 | }) 64 | //console.log('res', res) 65 | return res?.id 66 | } 67 | 68 | // upload avatar to file server v2 or open group server v2 69 | async function uploadEncryptedAvatar(baseUrl, serverPubkey, imgData) { 70 | // encryptProfile is still GCM 71 | //https://github.com/oxen-io/session-desktop/blob/clearnet/libtextsecure/crypto.js#L91 72 | const profileKeyBuf = crypto.randomBytes(32) // Buffer (object) 73 | const finalBuf = loki_crypto.encryptGCM(profileKeyBuf, imgData) 74 | 75 | // upload to server 76 | const fileId = await uploadFile(baseUrl, serverPubkey, finalBuf) 77 | 78 | // now we communicate it 79 | return { 80 | profileKeyBuf: profileKeyBuf, 81 | fileId: fileId, 82 | url: baseUrl + '/files/' + fileId + '?public_key=' + serverPubkey, 83 | } 84 | } 85 | 86 | // download avatar to file server v2 or open group server v2 87 | // returns a buffer 88 | async function downloadEncryptedAvatar(url, keyBuf, options = {}) { 89 | if (!Buffer.isBuffer(keyBuf)) { 90 | console.trace('lib::downloadEncryptedAvatar - non buffer passed in as key') 91 | return 92 | } 93 | if (!url) { 94 | console.trace('lib::downloadEncryptedAvatar - falsish url passed in') 95 | return 96 | } 97 | 98 | // parse URL into parts 99 | const urlDetails = new urlparser.URL(url) 100 | // no trailing slash 101 | const baseUrl = urlDetails.protocol + '//' + urlDetails.host 102 | const fileId = urlDetails.pathname.replace('/files/', '') 103 | const serverPubkeyHex = urlDetails.searchParams.get('public_key') 104 | const serverPubKey = serverPubkeyHex || options.pubkey 105 | const endpoint = 'files/' + fileId 106 | 107 | const obj = await lib.lsrpc(baseUrl, '', serverPubKey, endpoint, 'GET', '', {}) 108 | if (!obj || obj.status_code !== 200) { 109 | console.log('downloadEncryptedAvatar got non-200 result code', obj) 110 | return false 111 | } 112 | const ivCiphertextAndTag = Buffer.from(obj.result, 'base64') 113 | 114 | const fileBuf = loki_crypto.decryptGCM(keyBuf, ivCiphertextAndTag) 115 | return fileBuf 116 | } 117 | 118 | // FIXME: mime type, filename 119 | // untested 120 | async function uploadEncryptedAttachment(homeSrvUrl, serverPubkey, data) { 121 | //console.log('lib:::attachments::uploadEncryptedAttachment -', homeSrvUrl, serverPubkey, data) 122 | if (data === undefined) { 123 | // encryptCBC will fail 124 | console.trace('lib:::attachments::uploadEncryptedAttachment - data param is undefined') 125 | return 126 | } 127 | const keysBuf = crypto.randomBytes(64) // aes(32) and mac(32) 128 | const ivCiphertextAndMac = await loki_crypto.encryptCBC(keysBuf, data) 129 | 130 | // FIXME: these two actions can be done in parallel 131 | const fileId = await uploadFile(homeSrvUrl, serverPubkey, ivCiphertextAndMac) 132 | //console.log('lib:::attachments::uploadEncryptedAttachment - fileId', fileId) 133 | const digest = crypto.createHash('sha256').update(ivCiphertextAndMac).digest() 134 | // end 135 | return { 136 | id: fileId, // is this right? This is the only required field... 137 | contentType: 'image/jpeg', 138 | key: keysBuf.toString('base64'), 139 | size: data.byteLength, 140 | //thumbnail 141 | digest: digest.toString('base64'), 142 | fileName: 'images.jpeg', 143 | // flags (VOICE_MESSAGE or not) 144 | // width 145 | // height 146 | // caption 147 | url: homeSrvUrl + '/files/' + fileId + '?public_key=' + serverPubkey, 148 | } 149 | } 150 | 151 | // different than avatar because uses aes-CBC 152 | async function downloadEncryptedAttachment(url, keys, options = {}) { 153 | // parse URL into parts 154 | //console.debug('attachments::downloadEncryptedAttachment - url', url) 155 | const urlDetails = new urlparser.URL(url) 156 | // no trailing slash 157 | const baseUrl = urlDetails.protocol + '//' + urlDetails.host 158 | //console.debug('attachments::downloadEncryptedAttachment - pathname', urlDetails.pathname) 159 | const fileId = urlDetails.pathname.replace(/\/files?\//, '') 160 | //console.debug('attachments::downloadEncryptedAttachment - fileId', fileId) 161 | const serverPubkeyHex = urlDetails.searchParams.get('public_key') 162 | const serverPubKey = serverPubkeyHex || options.pubkey 163 | const endpoint = 'files/' + fileId 164 | 165 | // FIXME: may need a token for open groups 166 | 167 | const obj = await lib.lsrpc(baseUrl, '', serverPubKey, endpoint, 'GET', '', {}) 168 | if (!obj || obj.status_code !== 200) { 169 | console.log('downloadEncryptedAvatar got non-200 result code', obj) 170 | return false 171 | } 172 | const ivCiphertextAndMac = Buffer.from(obj.result, 'base64') 173 | 174 | // CBC strips the trailing mac off 175 | const fileBuf = loki_crypto.decryptCBC(keys, ivCiphertextAndMac) 176 | return fileBuf 177 | } 178 | 179 | module.exports = { 180 | // no longer possible 181 | //getAvatar, 182 | uploadEncryptedAvatar, 183 | downloadEncryptedAvatar, 184 | uploadEncryptedAttachment, 185 | downloadEncryptedAttachment 186 | } 187 | -------------------------------------------------------------------------------- /docs/lib_lns.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: lib/lns.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: lib/lns.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
const blake2bUtils = require('./blake2b.js')
 30 | const lib = require('./lib.js')
 31 | const _sodium = require('libsodium-wrappers')
 32 | 
 33 | /**
 34 |  * Loki Name Service Utilities
 35 |  * @module lns
 36 |  * @exports {object} exports.getNameSafe
 37 |  * @exports {object} exports.getNameFast
 38 |  * @author Ryan Tharp
 39 |  * @license ISC
 40 |  */
 41 | 
 42 | async function getSodium() {
 43 |   await _sodium.ready
 44 |   return _sodium
 45 | }
 46 | 
 47 | // good candiate of a primitive
 48 | /**
 49 |  * Lookup LNS name against three snode and get agreement
 50 |  * @param {String} lnsName what name to look up
 51 |  * @return {Promise<String>} pubkey (SessionID) it points to
 52 |  */
 53 | async function getNameSafe(lnsName) {
 54 |   const requests = [...Array(3).keys()]
 55 |   const list = await Promise.all(requests.map(idx => getNameFast(lnsName)))
 56 |   if (list.every(v => v === list[0])) {
 57 |     return list[0]
 58 |   }
 59 | }
 60 | 
 61 | /**
 62 |  * Lookup LNS name against one snode
 63 |  * @param {String} lnsName what name to look up
 64 |  * @return {Promise<String>} pubkey (SessionID) it points to
 65 |  */
 66 | async function getNameFast(lnsName) {
 67 |   const nameBuf = Buffer.from(lnsName)
 68 |   const uArr = blake2bUtils.blake2b(nameBuf, undefined, 32)
 69 |   const hash64 = Buffer.from(uArr).toString('base64')
 70 |   const snodeUrl = await lib.getRandomSnode()
 71 |   console.log('asking', snodeUrl, 'about', lnsName)
 72 |   const res = await lib.jsonrpc(snodeUrl, 'get_lns_mapping', {
 73 |     name_hash: hash64
 74 |   })
 75 |   console.log('decoding', lnsName, 'response from', snodeUrl)
 76 |   // FIXME: handle network failures better...
 77 |   /*
 78 | [
 79 |   {
 80 |     backup_owner: '',
 81 |     encrypted_value: '61b77686bbb8bed9074386598d6e18e0a9cca8098c1dd4b643a643cabfbf133ef9f0cdd01adf252bf18eba378f6de003d8',
 82 |     entry_index: 0,
 83 |     name_hash: 'CFm/zhpmu+SVenlQLPED6xzTja5L3ncc1KLw/+ewrUk=',
 84 |     owner: 'LBtSHQi85YEGRj1y87dHYRfEQPFwHGCRv3ceqesuSo3PKXW1LhHcFLCUSMHJ9hdRejcJRiQHX1WzdWsdBRRGJzA8QBMY27J',
 85 |     prev_txid: '',
 86 |     register_height: 497549,
 87 |     txid: 'bd1b9ca44ae541b277cdf62811012823911365c28fe978f38c39115a2b747e5a',
 88 |     type: 0,
 89 |     update_height: 497549
 90 |   }
 91 | ]
 92 |   */
 93 |   //console.log('res', res.result.entries)
 94 |   if (!res || !res.result || !res.result.entries) {
 95 |     console.warn('lib:::lns::getName - Error retrieving', res)
 96 |     return
 97 |   }
 98 |   if (res.result.entries.length !== 1) {
 99 |     console.warn('lib:::lns::getName - Too many entries', res.result.entries)
100 |     return
101 |   }
102 |   const obj = res.result.entries[0]
103 |   const sodium = await getSodium()
104 |   console.log('obj.encrypted_value.length', obj.encrypted_value.length)
105 | 
106 |   // old 7.x heavy encryption (xsalsa20-poly1305/argon2)
107 | 
108 |   // salt is all 0s (16 bytes)
109 |   const salt = new Uint8Array(sodium.crypto_pwhash_SALTBYTES)
110 |   //console.log(lnsName, 'salt', salt)
111 | 
112 |   // these are different
113 |   //console.log(lnsName, 'encrypted_value', obj.encrypted_value)
114 | 
115 |   //const cipherTextBuf = Buffer.from(obj.encrypted_value, 'hex')
116 |   // make sure it's uint8array
117 |   //const cipherText = new Uint8Array(cipherTextBuf.buffer, cipherTextBuf.byteOffset, cipherTextBuf.byteLength);
118 |   const cipherText = sodium.from_hex(obj.encrypted_value)
119 |   //console.log(lnsName, 'cipherText', cipherText) // 49 bytes...
120 |   // try to decrypt
121 |   try {
122 |     const key = sodium.crypto_pwhash(
123 |       sodium.crypto_secretbox_KEYBYTES,
124 |       lnsName, // key
125 |       salt,
126 |       sodium.crypto_pwhash_OPSLIMIT_MODERATE,
127 |       sodium.crypto_pwhash_MEMLIMIT_MODERATE,
128 |       sodium.crypto_pwhash_ALG_ARGON2ID13
129 |     )
130 | 
131 |     // nonce should be all 0s (24 bytes)
132 |     const nonce = new Uint8Array(sodium.crypto_secretbox_NONCEBYTES)
133 |     //console.log(lnsName, 'nonce', nonce)
134 | 
135 |     /*
136 |     const nonce = nonceAndCipherText.slice(0, sodium.crypto_secretbox_NONCEBYTES)
137 |     const cipherText = nonceAndCipherText.slice(sodium.crypto_secretbox_NONCEBYTES)
138 |     */
139 |     // this is uint8
140 |     const decryptedVal = sodium.crypto_secretbox_open_easy(cipherText, nonce, key)
141 |     // convert back to hex (it includes the 05 prefix since that's usually put into the LNS record)
142 |     console.log('Decoded', lnsName, 'from', snodeUrl)
143 |     return Buffer.from(decryptedVal).toString('hex')
144 |     // 053b6b764388cd6c4d38ae0b3e7492a8ecf0076e270c013bb5693d973045f45254 will be a common response
145 |     // means they haven't set their session id yet...
146 |   } catch (err) {
147 |     console.error('lib:::lns::getName - decryption err', err)
148 |   }
149 | }
150 | 
151 | module.exports = {
152 |   getNameSafe: getNameSafe,
153 |   getNameFast: getNameFast,
154 | }
155 | 
156 |
157 |
158 | 159 | 160 | 161 | 162 |
163 | 164 | 167 | 168 |
169 | 170 |
171 | Documentation generated by JSDoc 3.6.6 on Sat Jan 21 2023 00:39:15 GMT+0000 (Coordinated Universal Time) 172 |
173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /external/mnemonic/mnemonic.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | class MnemonicError extends Error {} 3 | 4 | let crc32 5 | /* eslint-disable */ 6 | if (typeof (module) === 'undefined') { 7 | // browser 8 | function loadFile(file, cb) { 9 | fetch('mnemonic/' + file).then(async resp => { 10 | const words = await resp.json() 11 | cb(words) 12 | }) 13 | } 14 | } else { 15 | // node 16 | function loadFile(file, cb) { 17 | cb(require('./' + file)) 18 | } 19 | } 20 | /* eslint-enable */ 21 | 22 | /* 23 | mnemonic.js : Converts between 4-byte aligned strings and a human-readable 24 | sequence of words. Uses 1626 common words taken from wikipedia article: 25 | http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry 26 | Originally written in python special for Electrum (lightweight Bitcoin client). 27 | This version has been reimplemented in javascript and placed in public domain. 28 | */ 29 | 30 | const mn_default_wordset = 'english' 31 | 32 | function mn_get_checksum_index(words, prefix_len) { 33 | let trimmed_words = '' 34 | for (let i = 0; i < words.length; i++) { 35 | trimmed_words += words[i].slice(0, prefix_len) 36 | } 37 | let signedChecksum 38 | if (typeof (module) === 'undefined') { 39 | // browser 40 | // eslint-disable-next-line no-undef 41 | signedChecksum = CRC32.str(trimmed_words) 42 | } else { 43 | // node 44 | signedChecksum = crc32.unsigned(trimmed_words) 45 | } 46 | const unsignedChecksum = (new Uint32Array([signedChecksum]))[0] 47 | const index = unsignedChecksum % words.length 48 | return index 49 | } 50 | 51 | let iAmReady 52 | const ready = new Promise(resolve => { 53 | iAmReady = resolve 54 | }) 55 | 56 | // hex, language => mnemonic 57 | async function mn_encode(str, wordset_name) { 58 | 'use strict' 59 | await ready 60 | wordset_name = wordset_name || mn_default_wordset 61 | const wordset = mn_words[wordset_name] 62 | let out = [] 63 | const n = wordset.words.length 64 | for (let j = 0; j < str.length; j += 8) { 65 | str = 66 | str.slice(0, j) + 67 | mn_swap_endian_4byte(str.slice(j, j + 8)) + 68 | str.slice(j + 8) 69 | } 70 | for (let i = 0; i < str.length; i += 8) { 71 | const x = parseInt(str.substr(i, 8), 16) 72 | const w1 = x % n 73 | const w2 = (Math.floor(x / n) + w1) % n 74 | const w3 = (Math.floor(Math.floor(x / n) / n) + w2) % n 75 | out = out.concat([wordset.words[w1], wordset.words[w2], wordset.words[w3]]) 76 | } 77 | if (wordset.prefix_len > 0) { 78 | out.push(out[mn_get_checksum_index(out, wordset.prefix_len)]) 79 | } 80 | return out.join(' ') 81 | } 82 | 83 | function mn_swap_endian_4byte(str) { 84 | 'use strict' 85 | if (str.length !== 8) { throw new MnemonicError('Invalid input length: ' + str.length) } 86 | return str.slice(6, 8) + str.slice(4, 6) + str.slice(2, 4) + str.slice(0, 2) 87 | } 88 | 89 | function mn_decode(str, wordset_name) { 90 | 'use strict' 91 | wordset_name = wordset_name || mn_default_wordset 92 | const wordset = mn_words[wordset_name] 93 | 94 | let out = '' 95 | const n = wordset.words.length 96 | const wlist = str.split(' ') 97 | let checksum_word = '' 98 | if (wlist.length < 12) { throw new MnemonicError("You've entered too few words, please try again") } 99 | if ( 100 | (wordset.prefix_len === 0 && wlist.length % 3 !== 0) || 101 | (wordset.prefix_len > 0 && wlist.length % 3 === 2) 102 | ) { throw new MnemonicError("You've entered too few words, please try again") } 103 | if (wordset.prefix_len > 0 && wlist.length % 3 === 0) { 104 | throw new MnemonicError( 105 | 'You seem to be missing the last word in your private key, please try again' 106 | ) 107 | } 108 | if (wordset.prefix_len > 0) { 109 | // Pop checksum from mnemonic 110 | checksum_word = wlist.pop() 111 | } 112 | // Decode mnemonic 113 | for (let i = 0; i < wlist.length; i += 3) { 114 | let w1, w2, w3 115 | if (wordset.prefix_len === 0) { 116 | w1 = wordset.words.indexOf(wlist[i]) 117 | w2 = wordset.words.indexOf(wlist[i + 1]) 118 | w3 = wordset.words.indexOf(wlist[i + 2]) 119 | } else { 120 | w1 = wordset.trunc_words.indexOf(wlist[i].slice(0, wordset.prefix_len)) 121 | w2 = wordset.trunc_words.indexOf( 122 | wlist[i + 1].slice(0, wordset.prefix_len) 123 | ) 124 | w3 = wordset.trunc_words.indexOf( 125 | wlist[i + 2].slice(0, wordset.prefix_len) 126 | ) 127 | } 128 | if (w1 === -1 || w2 === -1 || w3 === -1) { 129 | throw new MnemonicError('invalid word in mnemonic') 130 | } 131 | const x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n) 132 | if (x % n !== w1) { 133 | throw new MnemonicError( 134 | 'Something went wrong when decoding your private key, please try again' 135 | ) 136 | } 137 | out += mn_swap_endian_4byte(('0000000' + x.toString(16)).slice(-8)) 138 | } 139 | // Verify checksum 140 | if (wordset.prefix_len > 0) { 141 | const index = mn_get_checksum_index(wlist, wordset.prefix_len) 142 | const expected_checksum_word = wlist[index] 143 | if ( 144 | expected_checksum_word.slice(0, wordset.prefix_len) !== 145 | checksum_word.slice(0, wordset.prefix_len) 146 | ) { 147 | throw new MnemonicError( 148 | 'Your private key could not be verified, please verify the checksum word' 149 | ) 150 | } 151 | } 152 | return out 153 | } 154 | 155 | // Note: the value is the prefix_len 156 | const languages = { 157 | /* 158 | chinese_simplified: 1, 159 | dutch: 4, 160 | electrum: 0, 161 | */ 162 | english: 3 163 | /* 164 | esperanto: 4, 165 | french: 4, 166 | german: 4, 167 | italian: 4, 168 | japanese: 3, 169 | lojban: 4, 170 | portuguese: 4, 171 | russian: 4, 172 | spanish: 4, 173 | */ 174 | } 175 | 176 | const mn_words = {} 177 | for (const [language, prefix_len] of Object.entries(languages)) { 178 | // eslint-disable-next-line no-undef 179 | loadFile('english.json', function(words) { 180 | mn_words[language] = { 181 | prefix_len, 182 | words 183 | } 184 | 185 | for (const i in mn_words) { 186 | if (Object.prototype.hasOwnProperty.call(mn_words, i)) { 187 | if (mn_words[i].prefix_len === 0) { 188 | continue 189 | } 190 | mn_words[i].trunc_words = [] 191 | for (let j = 0; j < mn_words[i].words.length; ++j) { 192 | mn_words[i].trunc_words.push( 193 | mn_words[i].words[j].slice(0, mn_words[i].prefix_len) 194 | ) 195 | } 196 | } 197 | } 198 | 199 | iAmReady() 200 | }) 201 | } 202 | 203 | // node and browser compatibility 204 | ; // this semicolon is required 205 | (function(ref) { 206 | if (ref.constructor.name === 'Module') { 207 | // node 208 | crc32 = require('buffer-crc32') 209 | //global.Headers = fetch.Headers; 210 | module.exports = { 211 | mn_encode, 212 | mn_decode 213 | } 214 | } else { 215 | // browser 216 | // should be already set 217 | //window['crc32'] = 218 | } 219 | })(typeof (module) === 'undefined' ? this : module) 220 | -------------------------------------------------------------------------------- /lib/open_groups.js: -------------------------------------------------------------------------------- 1 | const lib = require('./lib.js') 2 | // eslint-disable-next-line camelcase 3 | const loki_crypto = require('./lib.loki_crypto.js') 4 | 5 | async function getToken(openGroupURL, privKey, pubkeyHex) { 6 | const openGroupUrl = `https://${openGroupURL}` 7 | const chalUrl = `${openGroupUrl}/loki/v1/get_challenge?pubKey=${pubkeyHex}` 8 | const data = await lib.jsonAsk(chalUrl) 9 | if (!data.cipherText64 || !data.serverPubKey64) { 10 | console.error('open_groups::getToken - data', typeof (data), data) 11 | return 12 | } 13 | // decode server public key 14 | const serverPubKeyBuff = Buffer.from(data.serverPubKey64, 'base64') 15 | // make sym key 16 | const symmetricKey = await loki_crypto.makeSymmetricKey(privKey, serverPubKeyBuff) 17 | // decrypt 18 | const tokenBuf = await loki_crypto.DHDecrypt64(symmetricKey, data.cipherText64) 19 | const token = tokenBuf.toString() // fix up type 20 | // set up submit to activate token 21 | const subUrl = `${openGroupUrl}/loki/v1/submit_challenge` 22 | let activateRes 23 | try { 24 | activateRes = await lib.textAsk(subUrl, { 25 | method: 'POST', 26 | body: JSON.stringify({ 27 | token: token, 28 | pubKey: pubkeyHex 29 | }), 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | } 33 | }) 34 | } catch (e) { 35 | console.error('open_groups::getToken - submit_challenge err', e) 36 | } 37 | if (activateRes !== '') { 38 | console.error('Failed to get token for', openGroupUrl, pubkeyHex) 39 | } 40 | return token 41 | } 42 | 43 | class SessionOpenGroupChannel { 44 | constructor(openGroupURL, options = {}) { 45 | this.channelId = options.channelId || 1 46 | this.serverUrl = openGroupURL 47 | this.lastId = 0 48 | this.timer = null 49 | this.pollRate = options.pollRate || 1000 50 | this.keypair = options.keypair || false 51 | this.token = options.token || '' 52 | this.pollServer = false 53 | } 54 | 55 | async subscribe() { 56 | console.log('Subscribing to Open Group', this.serverUrl) 57 | try { 58 | const subscriptionResult = await lib.jsonAsk(`https://${this.serverUrl}/channels/${this.channelId}/subscribe`, 59 | { 60 | method: 'post', 61 | headers: { 62 | Authorization: `Bearer ${this.token}`, 63 | 'Content-type': 'application/json', 64 | Accept: 'application/json', 65 | 'Accept-Charset': 'utf-8' 66 | } 67 | }) 68 | if (subscriptionResult.meta && subscriptionResult.meta.code && subscriptionResult.meta.code === 200) { 69 | return subscriptionResult 70 | } else { 71 | console.error('open_groups::subscribe - Wrong response received', subscriptionResult) 72 | } 73 | } catch (e) { 74 | console.error('open_groups::subscribe - Subscribe error', e) 75 | } 76 | return null 77 | } 78 | 79 | async getMessages() { 80 | try { 81 | const messageListResult = await lib.jsonAsk(`https://${this.serverUrl}/channels/${this.channelId}/messages?since_id=${this.lastId}`, 82 | { 83 | method: 'get', 84 | headers: { 85 | Authorization: `Bearer ${this.token}`, 86 | 'Content-type': 'application/json', 87 | Accept: 'application/json', 88 | 'Accept-Charset': 'utf-8' 89 | } 90 | }) 91 | if (messageListResult.meta && messageListResult.meta.code && messageListResult.meta.code === 200 && messageListResult.data) { 92 | if (messageListResult.data.length) { 93 | this.lastId = messageListResult.data[0].id 94 | } 95 | return messageListResult.data 96 | } else { 97 | console.error('open_groups::getMessages - Wrong response received', messageListResult) 98 | } 99 | } catch (e) { 100 | console.error('open_groups::getMessages - Getting messages error', e) 101 | } 102 | return null 103 | } 104 | 105 | async send(text, options = {}) { 106 | try { 107 | const sigVer = 1 108 | const mockAdnMessage = { text } 109 | const timestamp = new Date().getTime() 110 | const annotations = [ 111 | { 112 | type: 'network.loki.messenger.publicChat', 113 | value: { 114 | timestamp, 115 | // sig: '6b07d9f8c7bb4c5e28a43b4dd2aa4889405361e709258a0420ba55c8aa6784c1b3059787a7adeec85bbce66832fa61efa7398a55ee9f45aa396a9c05f9edb105', 116 | //sigver: sigVer 117 | } 118 | 119 | } 120 | ] 121 | if (options && options.avatar) { 122 | // inject avatar is we have it... 123 | // probably should do it differently... 124 | annotations[0].value.avatar = options.avatar 125 | } 126 | 127 | const sig = await loki_crypto.getSigData( 128 | sigVer, 129 | this.keypair.privKey, 130 | annotations[0].value, 131 | mockAdnMessage 132 | ) 133 | 134 | annotations[0].value.sig = sig 135 | annotations[0].value.sigver = sigVer 136 | 137 | const payload = { 138 | text, 139 | annotations 140 | } 141 | const messageSendResult = await lib.jsonAsk(`https://${this.serverUrl}/channels/${this.channelId}/messages`, 142 | { 143 | method: 'POST', 144 | body: JSON.stringify(payload), 145 | headers: { 146 | Authorization: `Bearer ${this.token}`, 147 | 'Content-type': 'application/json', 148 | Accept: 'application/json', 149 | 'Accept-Charset': 'utf-8' 150 | } 151 | }) 152 | //console.log(messageSendResult) 153 | if (messageSendResult.meta && messageSendResult.meta.code && messageSendResult.meta.code === 200 && messageSendResult.data) { 154 | return messageSendResult.data 155 | } else { 156 | console.error('open_groups::send - Wrong response received', messageSendResult) 157 | } 158 | } catch (e) { 159 | console.error('open_groups::send - Sending messages error', e) 160 | } 161 | return null 162 | } 163 | 164 | async messageDelete(messageIds = []) { 165 | try { 166 | const messageDeleteResult = await lib.jsonAsk(`https://${this.serverUrl}/loki/v1/moderation/messages?ids=${encodeURIComponent(messageIds)}`, 167 | { 168 | method: 'DELETE', 169 | headers: { 170 | Authorization: `Bearer ${this.token}`, 171 | 'Content-type': 'application/json', 172 | Accept: 'application/json', 173 | 'Accept-Charset': 'utf-8' 174 | } 175 | }) 176 | if (messageDeleteResult.meta && messageDeleteResult.meta.code && messageDeleteResult.meta.code === 200 && messageDeleteResult.data) { 177 | return messageDeleteResult.data 178 | } else { 179 | console.error('open_groups::delete - Wrong response received', messageDeleteResult) 180 | } 181 | } catch (e) { 182 | console.error('open_groups::delete - Getting messages error', e) 183 | } 184 | return null 185 | } 186 | } 187 | 188 | module.exports = { 189 | getToken, 190 | SessionOpenGroupChannel 191 | } 192 | -------------------------------------------------------------------------------- /docs/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('../fonts/OpenSans-Regular-webfont.eot'); 6 | src: 7 | local('Open Sans'), 8 | local('OpenSans'), 9 | url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), 11 | url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Open Sans Light'; 16 | font-weight: normal; 17 | font-style: normal; 18 | src: url('../fonts/OpenSans-Light-webfont.eot'); 19 | src: 20 | local('Open Sans Light'), 21 | local('OpenSans Light'), 22 | url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('../fonts/OpenSans-Light-webfont.woff') format('woff'), 24 | url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); 25 | } 26 | 27 | html 28 | { 29 | overflow: auto; 30 | background-color: #fff; 31 | font-size: 14px; 32 | } 33 | 34 | body 35 | { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.5; 38 | color: #4d4e53; 39 | background-color: white; 40 | } 41 | 42 | a, a:visited, a:active { 43 | color: #0095dd; 44 | text-decoration: none; 45 | } 46 | 47 | a:hover { 48 | text-decoration: underline; 49 | } 50 | 51 | header 52 | { 53 | display: block; 54 | padding: 0px 4px; 55 | } 56 | 57 | tt, code, kbd, samp { 58 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 59 | } 60 | 61 | .class-description { 62 | font-size: 130%; 63 | line-height: 140%; 64 | margin-bottom: 1em; 65 | margin-top: 1em; 66 | } 67 | 68 | .class-description:empty { 69 | margin: 0; 70 | } 71 | 72 | #main { 73 | float: left; 74 | width: 70%; 75 | } 76 | 77 | article dl { 78 | margin-bottom: 40px; 79 | } 80 | 81 | article img { 82 | max-width: 100%; 83 | } 84 | 85 | section 86 | { 87 | display: block; 88 | background-color: #fff; 89 | padding: 12px 24px; 90 | border-bottom: 1px solid #ccc; 91 | margin-right: 30px; 92 | } 93 | 94 | .variation { 95 | display: none; 96 | } 97 | 98 | .signature-attributes { 99 | font-size: 60%; 100 | color: #aaa; 101 | font-style: italic; 102 | font-weight: lighter; 103 | } 104 | 105 | nav 106 | { 107 | display: block; 108 | float: right; 109 | margin-top: 28px; 110 | width: 30%; 111 | box-sizing: border-box; 112 | border-left: 1px solid #ccc; 113 | padding-left: 16px; 114 | } 115 | 116 | nav ul { 117 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 118 | font-size: 100%; 119 | line-height: 17px; 120 | padding: 0; 121 | margin: 0; 122 | list-style-type: none; 123 | } 124 | 125 | nav ul a, nav ul a:visited, nav ul a:active { 126 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 127 | line-height: 18px; 128 | color: #4D4E53; 129 | } 130 | 131 | nav h3 { 132 | margin-top: 12px; 133 | } 134 | 135 | nav li { 136 | margin-top: 6px; 137 | } 138 | 139 | footer { 140 | display: block; 141 | padding: 6px; 142 | margin-top: 12px; 143 | font-style: italic; 144 | font-size: 90%; 145 | } 146 | 147 | h1, h2, h3, h4 { 148 | font-weight: 200; 149 | margin: 0; 150 | } 151 | 152 | h1 153 | { 154 | font-family: 'Open Sans Light', sans-serif; 155 | font-size: 48px; 156 | letter-spacing: -2px; 157 | margin: 12px 24px 20px; 158 | } 159 | 160 | h2, h3.subsection-title 161 | { 162 | font-size: 30px; 163 | font-weight: 700; 164 | letter-spacing: -1px; 165 | margin-bottom: 12px; 166 | } 167 | 168 | h3 169 | { 170 | font-size: 24px; 171 | letter-spacing: -0.5px; 172 | margin-bottom: 12px; 173 | } 174 | 175 | h4 176 | { 177 | font-size: 18px; 178 | letter-spacing: -0.33px; 179 | margin-bottom: 12px; 180 | color: #4d4e53; 181 | } 182 | 183 | h5, .container-overview .subsection-title 184 | { 185 | font-size: 120%; 186 | font-weight: bold; 187 | letter-spacing: -0.01em; 188 | margin: 8px 0 3px 0; 189 | } 190 | 191 | h6 192 | { 193 | font-size: 100%; 194 | letter-spacing: -0.01em; 195 | margin: 6px 0 3px 0; 196 | font-style: italic; 197 | } 198 | 199 | table 200 | { 201 | border-spacing: 0; 202 | border: 0; 203 | border-collapse: collapse; 204 | } 205 | 206 | td, th 207 | { 208 | border: 1px solid #ddd; 209 | margin: 0px; 210 | text-align: left; 211 | vertical-align: top; 212 | padding: 4px 6px; 213 | display: table-cell; 214 | } 215 | 216 | thead tr 217 | { 218 | background-color: #ddd; 219 | font-weight: bold; 220 | } 221 | 222 | th { border-right: 1px solid #aaa; } 223 | tr > th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .source 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .source code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /docs/module-lns.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: lns 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Module: lns

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 | 41 |
Loki Name Service Utilities
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
Author:
81 |
82 |
    83 |
  • Ryan Tharp
  • 84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 |
License:
92 |
  • ISC
93 | 94 | 95 | 96 | 97 | 98 |
Source:
99 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |

Methods

150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |

(async, inner) getNameFast(lnsName) → {Promise.<String>}

158 | 159 | 160 | 161 | 162 | 163 | 164 |
165 | Lookup LNS name against one snode 166 |
167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
Parameters:
177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
NameTypeDescription
lnsName 205 | 206 | 207 | String 208 | 209 | 210 | 211 | what name to look up
223 | 224 | 225 | 226 | 227 | 228 | 229 |
230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 |
Source:
257 |
260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 |
268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 |
Returns:
284 | 285 | 286 |
287 | pubkey (SessionID) it points to 288 |
289 | 290 | 291 | 292 |
293 |
294 | Type 295 |
296 |
297 | 298 | Promise.<String> 299 | 300 | 301 |
302 |
303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 |

(async, inner) getNameSafe(lnsName) → {Promise.<String>}

317 | 318 | 319 | 320 | 321 | 322 | 323 |
324 | Lookup LNS name against three snode and get agreement 325 |
326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 |
Parameters:
336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 |
NameTypeDescription
lnsName 364 | 365 | 366 | String 367 | 368 | 369 | 370 | what name to look up
382 | 383 | 384 | 385 | 386 | 387 | 388 |
389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 |
Source:
416 |
419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 |
427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 |
Returns:
443 | 444 | 445 |
446 | pubkey (SessionID) it points to 447 |
448 | 449 | 450 | 451 |
452 |
453 | Type 454 |
455 |
456 | 457 | Promise.<String> 458 | 459 | 460 |
461 |
462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 |
476 | 477 |
478 | 479 | 480 | 481 | 482 |
483 | 484 | 487 | 488 |
489 | 490 |
491 | Documentation generated by JSDoc 3.6.6 on Sat Jan 21 2023 00:39:15 GMT+0000 (Coordinated Universal Time) 492 |
493 | 494 | 495 | 496 | 497 | -------------------------------------------------------------------------------- /lib/blake2b.js: -------------------------------------------------------------------------------- 1 | // from https://github.com/dcposch/blakejs/blob/master/blake2b.js 2 | // CC-0 3 | 4 | // For convenience, let people hash a string, not just a Uint8Array 5 | function normalizeInput(input) { 6 | let ret 7 | if (input instanceof Uint8Array) { 8 | ret = input 9 | } else if (input instanceof Buffer) { 10 | ret = new Uint8Array(input) 11 | } else if (typeof (input) === 'string') { 12 | ret = new Uint8Array(Buffer.from(input, 'utf8')) 13 | } else { 14 | throw new Error('normalizeInput Error') 15 | } 16 | return ret 17 | } 18 | 19 | // Converts a Uint8Array to a hexadecimal string 20 | // For example, toHex([255, 0, 255]) returns "ff00ff" 21 | function toHex(bytes) { 22 | return Array.prototype.map.call(bytes, function(n) { 23 | return (n < 16 ? '0' : '') + n.toString(16) 24 | }).join('') 25 | } 26 | 27 | // 64-bit unsigned addition 28 | // Sets v[a,a+1] += v[b,b+1] 29 | // v should be a Uint32Array 30 | function ADD64AA(v, a, b) { 31 | const o0 = v[a] + v[b] 32 | let o1 = v[a + 1] + v[b + 1] 33 | if (o0 >= 0x100000000) { 34 | o1++ 35 | } 36 | v[a] = o0 37 | v[a + 1] = o1 38 | } 39 | 40 | // 64-bit unsigned addition 41 | // Sets v[a,a+1] += b 42 | // b0 is the low 32 bits of b, b1 represents the high 32 bits 43 | function ADD64AC(v, a, b0, b1) { 44 | let o0 = v[a] + b0 45 | if (b0 < 0) { 46 | o0 += 0x100000000 47 | } 48 | let o1 = v[a + 1] + b1 49 | if (o0 >= 0x100000000) { 50 | o1++ 51 | } 52 | v[a] = o0 53 | v[a + 1] = o1 54 | } 55 | 56 | // Little-endian byte access 57 | function B2B_GET32(arr, i) { 58 | return (arr[i] ^ 59 | (arr[i + 1] << 8) ^ 60 | (arr[i + 2] << 16) ^ 61 | (arr[i + 3] << 24)) 62 | } 63 | 64 | // G Mixing function 65 | // The ROTRs are inlined for speed 66 | function B2B_G(a, b, c, d, ix, iy) { 67 | const x0 = m[ix] 68 | const x1 = m[ix + 1] 69 | const y0 = m[iy] 70 | const y1 = m[iy + 1] 71 | 72 | ADD64AA(v, a, b) // v[a,a+1] += v[b,b+1] ... in JS we must store a uint64 as two uint32s 73 | ADD64AC(v, a, x0, x1) // v[a, a+1] += x ... x0 is the low 32 bits of x, x1 is the high 32 bits 74 | 75 | // v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated to the right by 32 bits 76 | let xor0 = v[d] ^ v[a] 77 | let xor1 = v[d + 1] ^ v[a + 1] 78 | v[d] = xor1 79 | v[d + 1] = xor0 80 | 81 | ADD64AA(v, c, d) 82 | 83 | // v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 24 bits 84 | xor0 = v[b] ^ v[c] 85 | xor1 = v[b + 1] ^ v[c + 1] 86 | v[b] = (xor0 >>> 24) ^ (xor1 << 8) 87 | v[b + 1] = (xor1 >>> 24) ^ (xor0 << 8) 88 | 89 | ADD64AA(v, a, b) 90 | ADD64AC(v, a, y0, y1) 91 | 92 | // v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated right by 16 bits 93 | xor0 = v[d] ^ v[a] 94 | xor1 = v[d + 1] ^ v[a + 1] 95 | v[d] = (xor0 >>> 16) ^ (xor1 << 16) 96 | v[d + 1] = (xor1 >>> 16) ^ (xor0 << 16) 97 | 98 | ADD64AA(v, c, d) 99 | 100 | // v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 63 bits 101 | xor0 = v[b] ^ v[c] 102 | xor1 = v[b + 1] ^ v[c + 1] 103 | v[b] = (xor1 >>> 31) ^ (xor0 << 1) 104 | v[b + 1] = (xor0 >>> 31) ^ (xor1 << 1) 105 | } 106 | 107 | // Initialization Vector 108 | const BLAKE2B_IV32 = new Uint32Array([ 109 | 0xF3BCC908, 0x6A09E667, 0x84CAA73B, 0xBB67AE85, 110 | 0xFE94F82B, 0x3C6EF372, 0x5F1D36F1, 0xA54FF53A, 111 | 0xADE682D1, 0x510E527F, 0x2B3E6C1F, 0x9B05688C, 112 | 0xFB41BD6B, 0x1F83D9AB, 0x137E2179, 0x5BE0CD19 113 | ]) 114 | 115 | const SIGMA8 = [ 116 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 117 | 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, 118 | 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4, 119 | 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 120 | 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 121 | 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9, 122 | 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 123 | 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 124 | 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, 125 | 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 126 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 127 | 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 128 | ] 129 | 130 | // These are offsets into a uint64 buffer. 131 | // Multiply them all by 2 to make them offsets into a uint32 buffer, 132 | // because this is Javascript and we don't have uint64s 133 | const SIGMA82 = new Uint8Array(SIGMA8.map(function(x) { return x * 2 })) 134 | 135 | // Compression function. 'last' flag indicates last block. 136 | // Note we're representing 16 uint64s as 32 uint32s 137 | const v = new Uint32Array(32) 138 | const m = new Uint32Array(32) 139 | function blake2bCompress(ctx, last) { 140 | let i = 0 141 | 142 | // init work variables 143 | for (i = 0; i < 16; i++) { 144 | v[i] = ctx.h[i] 145 | v[i + 16] = BLAKE2B_IV32[i] 146 | } 147 | 148 | // low 64 bits of offset 149 | v[24] = v[24] ^ ctx.t 150 | v[25] = v[25] ^ (ctx.t / 0x100000000) 151 | // high 64 bits not supported, offset may not be higher than 2**53-1 152 | 153 | // last block flag set ? 154 | if (last) { 155 | v[28] = ~v[28] 156 | v[29] = ~v[29] 157 | } 158 | 159 | // get little-endian words 160 | for (i = 0; i < 32; i++) { 161 | m[i] = B2B_GET32(ctx.b, 4 * i) 162 | } 163 | 164 | // twelve rounds of mixing 165 | // uncomment the DebugPrint calls to log the computation 166 | // and match the RFC sample documentation 167 | // util.debugPrint(' m[16]', m, 64) 168 | for (i = 0; i < 12; i++) { 169 | // util.debugPrint(' (i=' + (i < 10 ? ' ' : '') + i + ') v[16]', v, 64) 170 | B2B_G(0, 8, 16, 24, SIGMA82[i * 16 + 0], SIGMA82[i * 16 + 1]) 171 | B2B_G(2, 10, 18, 26, SIGMA82[i * 16 + 2], SIGMA82[i * 16 + 3]) 172 | B2B_G(4, 12, 20, 28, SIGMA82[i * 16 + 4], SIGMA82[i * 16 + 5]) 173 | B2B_G(6, 14, 22, 30, SIGMA82[i * 16 + 6], SIGMA82[i * 16 + 7]) 174 | B2B_G(0, 10, 20, 30, SIGMA82[i * 16 + 8], SIGMA82[i * 16 + 9]) 175 | B2B_G(2, 12, 22, 24, SIGMA82[i * 16 + 10], SIGMA82[i * 16 + 11]) 176 | B2B_G(4, 14, 16, 26, SIGMA82[i * 16 + 12], SIGMA82[i * 16 + 13]) 177 | B2B_G(6, 8, 18, 28, SIGMA82[i * 16 + 14], SIGMA82[i * 16 + 15]) 178 | } 179 | // util.debugPrint(' (i=12) v[16]', v, 64) 180 | 181 | for (i = 0; i < 16; i++) { 182 | ctx.h[i] = ctx.h[i] ^ v[i] ^ v[i + 16] 183 | } 184 | // util.debugPrint('h[8]', ctx.h, 64) 185 | } 186 | 187 | // Creates a BLAKE2b hashing context 188 | // Requires an output length between 1 and 64 bytes 189 | // Takes an optional Uint8Array key 190 | function blake2bInit(outlen, key) { 191 | if (outlen === 0 || outlen > 64) { 192 | throw new Error('Illegal output length, expected 0 < length <= 64') 193 | } 194 | if (key && key.length > 64) { 195 | throw new Error('Illegal key, expected Uint8Array with 0 < length <= 64') 196 | } 197 | 198 | // state, 'param block' 199 | const ctx = { 200 | b: new Uint8Array(128), 201 | h: new Uint32Array(16), 202 | t: 0, // input count 203 | c: 0, // pointer within buffer 204 | outlen: outlen // output length in bytes 205 | } 206 | 207 | // initialize hash state 208 | for (let i = 0; i < 16; i++) { 209 | ctx.h[i] = BLAKE2B_IV32[i] 210 | } 211 | const keylen = key ? key.length : 0 212 | ctx.h[0] ^= 0x01010000 ^ (keylen << 8) ^ outlen 213 | 214 | // key the hash, if applicable 215 | if (key) { 216 | blake2bUpdate(ctx, key) 217 | // at the end 218 | ctx.c = 128 219 | } 220 | 221 | return ctx 222 | } 223 | 224 | // Updates a BLAKE2b streaming hash 225 | // Requires hash context and Uint8Array (byte array) 226 | function blake2bUpdate(ctx, input) { 227 | for (let i = 0; i < input.length; i++) { 228 | if (ctx.c === 128) { // buffer full ? 229 | ctx.t += ctx.c // add counters 230 | blake2bCompress(ctx, false) // compress (not last) 231 | ctx.c = 0 // counter to zero 232 | } 233 | ctx.b[ctx.c++] = input[i] 234 | } 235 | } 236 | 237 | // Completes a BLAKE2b streaming hash 238 | // Returns a Uint8Array containing the message digest 239 | function blake2bFinal(ctx) { 240 | ctx.t += ctx.c // mark last block offset 241 | 242 | while (ctx.c < 128) { // fill up with zeros 243 | ctx.b[ctx.c++] = 0 244 | } 245 | blake2bCompress(ctx, true) // final block flag = 1 246 | 247 | // little endian convert and store 248 | const out = new Uint8Array(ctx.outlen) 249 | for (let i = 0; i < ctx.outlen; i++) { 250 | out[i] = ctx.h[i >> 2] >> (8 * (i & 3)) 251 | } 252 | return out 253 | } 254 | 255 | // Computes the BLAKE2B hash of a string or byte array, and returns a Uint8Array 256 | // 257 | // Returns a n-byte Uint8Array 258 | // 259 | // Parameters: 260 | // - input - the input bytes, as a string, Buffer or Uint8Array 261 | // - key - optional key Uint8Array, up to 64 bytes 262 | // - outlen - optional output length in bytes, default 64 263 | function blake2b(input, key, outlen) { 264 | // preprocess inputs 265 | outlen = outlen || 64 266 | input = normalizeInput(input) 267 | 268 | // do the math 269 | const ctx = blake2bInit(outlen, key) 270 | blake2bUpdate(ctx, input) 271 | return blake2bFinal(ctx) 272 | } 273 | 274 | // Computes the BLAKE2B hash of a string or byte array 275 | // 276 | // Returns an n-byte hash in hex, all lowercase 277 | // 278 | // Parameters: 279 | // - input - the input bytes, as a string, Buffer, or Uint8Array 280 | // - key - optional key Uint8Array, up to 64 bytes 281 | // - outlen - optional output length in bytes, default 64 282 | function blake2bHex(input, key, outlen) { 283 | const output = blake2b(input, key, outlen) 284 | return toHex(output) 285 | } 286 | 287 | module.exports = { 288 | blake2b: blake2b, 289 | blake2bHex: blake2bHex, 290 | blake2bInit: blake2bInit, 291 | blake2bUpdate: blake2bUpdate, 292 | blake2bFinal: blake2bFinal 293 | } 294 | -------------------------------------------------------------------------------- /lib/recv.js: -------------------------------------------------------------------------------- 1 | const protobuf = require('./protobuf.js') 2 | const _sodium = require('libsodium-wrappers') // maybe put in session-client? 3 | 4 | async function handleUnidentifiedMessageType(env, ourKeypair) { 5 | await _sodium.ready 6 | const sodium = _sodium 7 | 8 | // decode session protocol 9 | 10 | // 1. decrypt message 11 | const stripedPK = ourKeypair.pubKey.subarray(1) 12 | let plaintext, senderX25519PublicKey 13 | try { 14 | // sometimes get an incorrect keypair here... 15 | const plaintextWithMetadata = sodium.crypto_box_seal_open( 16 | new Uint8Array(env.content), 17 | stripedPK, // strip 05 18 | ourKeypair.privKey 19 | ) 20 | // integrity check 21 | const minSize = sodium.crypto_sign_BYTES + sodium.crypto_sign_PUBLICKEYBYTES 22 | if (plaintextWithMetadata.byteLength <= minSize) { 23 | console.error('decryption failed', plaintextWithMetadata.byteLength, 'is less than', minSize) 24 | return false 25 | } 26 | 27 | // 2. get message parts 28 | const metadataPos = plaintextWithMetadata.byteLength - minSize 29 | plaintext = plaintextWithMetadata.subarray(0, metadataPos) 30 | const signPos = plaintextWithMetadata.byteLength - sodium.crypto_sign_BYTES 31 | const senderED25519PublicKey = plaintextWithMetadata.subarray(metadataPos, signPos) 32 | const signature = plaintextWithMetadata.subarray(signPos) 33 | 34 | // 3. verify sig 35 | const isValid = sodium.crypto_sign_verify_detached( 36 | signature, 37 | Buffer.concat([plaintext, senderED25519PublicKey, stripedPK]), 38 | senderED25519PublicKey 39 | ) 40 | if (!isValid) { 41 | console.error('decryption failed - bad signature') 42 | return false 43 | } 44 | 45 | // 4. get senders pubkey 46 | senderX25519PublicKey = sodium.crypto_sign_ed25519_pk_to_curve25519( 47 | senderED25519PublicKey 48 | ) // Uint8Array 49 | if (!senderX25519PublicKey) { 50 | console.error('decryption failed - curve conversion') 51 | return false 52 | } 53 | } catch (e) { 54 | console.error('recv failure', e) 55 | return 56 | } 57 | 58 | if (!plaintext) { 59 | console.log('decrypt failure? no plaintext') 60 | return 61 | } 62 | 63 | // need to unpad plaintext 64 | const content = protobuf.decodeContentMessage(plaintext) 65 | return { ...content, source: `05${Buffer.from(senderX25519PublicKey).toString('hex')}` } 66 | } 67 | 68 | const decodeMessageMap = { 69 | // UNIDENTIFIED_SENDER 70 | 6: handleUnidentifiedMessageType 71 | } 72 | 73 | async function handleMessage(msg, ourKeypair) { 74 | // encode message as base64 and then uint8array 75 | const buf = Buffer.from(msg.data, 'base64') 76 | 77 | // handle ws/wsr 78 | let wsMessage 79 | try { 80 | wsMessage = protobuf.WebSocketMessage.decode(buf) 81 | } catch (e) { 82 | console.error('recv err, wsm', e) 83 | return 84 | } 85 | // now turn message.request.body into an envelope 86 | /* 87 | message WebSocketMessage { 88 | type: 1, 89 | request: 90 | WebSocketRequestMessage { 91 | headers: [], 92 | verb: 'PUT', 93 | path: '/api/v1/message', 94 | body: 95 | , 96 | id: Long { low: -347036081, high: 1342692244, unsigned: true } } } 97 | */ 98 | //console.log('wsMessage', wsMessage) 99 | if (wsMessage.type !== 1 || !wsMessage.request) { 100 | console.warn('unhandled websocket message', wsMessage) 101 | return 102 | } 103 | // recv/contentMessage.ts - handle envelope contnet 104 | const env = protobuf.Envelope.decode(wsMessage.request.body) 105 | /* 106 | Envelope { 107 | type: 6, 108 | timestamp: Long { low: -1066270830, high: 371, unsigned: true }, 109 | sourceDevice: 1, 110 | content: 111 | } 112 | */ 113 | //console.log('env', env) 114 | //console.log('env timestamp', env.timestamp.toString()) 115 | if (decodeMessageMap[env.type]) { 116 | const res = await decodeMessageMap[env.type](env, ourKeypair) 117 | if (res) { 118 | //console.log('res', res) 119 | return {...res, snodeExp: msg.expiration, hash: msg.hash, timestamp: env.timestamp.toString() } 120 | } else { 121 | return {} 122 | } 123 | } else { 124 | console.warn('unhandled envelope type', env.type) 125 | } 126 | } 127 | 128 | /* 129 | pubKeyAsk start https://54.39.15.185:22120/storage_rpc/v1 05aba1ad0ac5f3f5dbd14c54e81ce2a26c58e6fa2b2a901d40eb85246c3ce31b22 xJwZ9reNcRjeJZhLriFxvo3aURsokOsm0c0YN6S7a1c 130 | recv::checkBox - foundHash xJwZ9reNcRjeJZhLriFxvo3aURsokOsm0c0YN6S7a1c in results? false 131 | 1669959312621 SessionClient::poll - recvLib got { 132 | lastHash: 'xJwZ9reNcRjeJZhLriFxvo3aURsokOsm0c0YN6S7a1c', 133 | messages: [] 134 | } 135 | pubKeyAsk start https://149.56.113.46:22106/storage_rpc/v1 05aba1ad0ac5f3f5dbd14c54e81ce2a26c58e6fa2b2a901d40eb85246c3ce31b22 xJwZ9reNcRjeJZhLriFxvo3aURsokOsm0c0YN6S7a1c 136 | recv::checkBox - foundHash xJwZ9reNcRjeJZhLriFxvo3aURsokOsm0c0YN6S7a1c in results? false 137 | 1669959343209 SessionClient::poll - recvLib got { 138 | lastHash: 'alN/b0GFAC6FH7jWQtTwXtZ7bkNYT1xK40fvenM4uAY', 139 | messages: [ 140 | { 141 | dataMessage: [DataMessage], 142 | source: '05b69cc33267ada004c60677f39bcc3db91f1cfd841c0a3226a2b2bcc062d28959', 143 | snodeExp: 1671168929902, 144 | hash: 'U/wmKjLCEr9ph09APgyTNefmV1b4G+pMdXf6bgVSUgw' 145 | }, 146 | { 147 | dataMessage: [DataMessage], 148 | source: '05b69cc33267ada004c60677f39bcc3db91f1cfd841c0a3226a2b2bcc062d28959', 149 | snodeExp: 1671168929901, 150 | hash: 'alN/b0GFAC6FH7jWQtTwXtZ7bkNYT1xK40fvenM4uAY' 151 | } 152 | ] 153 | } 154 | setLast alN/b0GFAC6FH7jWQtTwXtZ7bkNYT1xK40fvenM4uAY 155 | 1669959343565 DM from 05b69cc33267ada004c60677f39bcc3db91f1cfd841c0a3226a2b2bcc062d28959 Vector12ProMax: 1234567890xffa 156 | 1669959343565 DM from 05b69cc33267ada004c60677f39bcc3db91f1cfd841c0a3226a2b2bcc062d28959 Vector12ProMax: 1234567890xffa 157 | */ 158 | 159 | async function checkBox(pubKey, ourKeypair, inLasthash, lib, debug) { 160 | if (!ourKeypair) { 161 | console.trace('lib::recv - no ourKeypair') 162 | process.exit(1) 163 | } 164 | if (inLasthash === null) { 165 | console.trace('recv::checkBox - inLasthash can not be null') 166 | inLasthash = undefined 167 | } 168 | //console.log('snodes', snodeData.snodes) 169 | const url = await lib.getSwarmsnodeUrl(pubKey, ourKeypair) 170 | if (debug) console.debug('pubKeyAsk start', url, pubKey, inLasthash) 171 | const messageData = await lib.pubKeyAsk(url, 'retrieve', pubKey, ourKeypair, { 172 | lastHash: inLasthash 173 | }) 174 | //console.log('messageData', messageData) 175 | //if (debug) console.debug('pubKeyAsk end') 176 | if (!messageData) { 177 | //console.error('recv::checkBox - no messageData') 178 | return 179 | } 180 | if (!messageData.messages) { 181 | // Service node is not ready: not in any swarm; not done syncing; 182 | console.debug('(missing messages) messageData', messageData) 183 | return { 184 | lastHash: inLasthash, 185 | messages: [] 186 | } 187 | } 188 | // go through the array and look for inLastHash 189 | // if found, start there... 190 | const foundHash = messageData.messages.some(msg => { 191 | return msg.hash === inLasthash 192 | }) 193 | if (debug) console.debug('recv::checkBox - foundHash', inLasthash, 'in results?', foundHash) 194 | if (foundHash) { 195 | const nMsgs = [] 196 | let hit = false 197 | for (const i in messageData.messages) { 198 | const msg = messageData.messages[i] 199 | if (msg.hash === inLasthash) { 200 | hit = true 201 | continue 202 | } 203 | if (hit) { 204 | nMsgs.push(msg) 205 | } 206 | } 207 | if (debug) console.debug('found hash in', messageData.messages.length, 'reduced down to', nMsgs.length) 208 | messageData.messages = nMsgs 209 | } 210 | 211 | let newMsgs = [] 212 | // when initial lastHash is empty, this will be empty 213 | // and even those it will list a bunch of records 214 | // it will leave it undefined 215 | let useLastHash = inLasthash 216 | // just make sure it's an array 217 | if (messageData.messages && messageData.messages.length) { 218 | //console.log('got', messageData.messages.length, 'msgs for', pubKey) 219 | newMsgs = await Promise.all(messageData.messages.map(async msg => { 220 | /* 221 | { data: 222 | 'CAESrgQKA1BVVBIPL2FwaS92MS9tZXNzYWdlGosECAYSACioz+7buy44AEL7AxEKIQXBLxkB745ij47zWCVCR97MXUPjoXDdIh73x2hjydWUQxIrIcl/2e/kdA0xfFj9hyDLkv9yHxdPSmsKxBpZOPyX0nDuHyHdnICArMjNixqnAx0UKnuvmKiPvUh5To22BbpgZ6L4GJwLjL2t/KtX4ufQZ955wzCseM9r+1E0/1cWf6uqax1nQbcAkPiqrMTzoQ+9r02HA5DrYRdI5azyQjB7uOHYcEmBmXj3mW93gA53jD6ohkxdFDEd0Jimo69geQug/iksB5/eLUbcT1lQjWYbsu0U22lvqv6vSTUEzBJcP46fcrLoBt1nb2KkZv5okGETQISc2RLBd1hIctitclhReXWvc/sKtXaU49sCCdR6EjzoKLQojIy0+yPjBDyK7n+Io/gnPPBU4t78QxuWxAQuRvb1x+xU/NLHEosNa9ArnCbzlDwXyyYRhFv6d4W3Q4fXy/Bbsu6rj3S/vomgtQPdt9Xu6vwn/eoWeKsB9PV7ON19AYRhOeDdhM5lMcg8SvyT/dKSNlInadEeiuTRxpDJGQ3yC1CkMwo8BEpnr64XMQ14UvZOC2/JLBMFTHQIbRcu96RJdseUb9edtW7uyxEEu/fve9NZWAPb3tX0m288ZzpoIH5PgXLRzhN0Z68CyoeCEuO7RU7B6TKqW8H7DBZgnbIv/ECFeyDPzMLazvL2g1A=', 223 | expiration: 1596747510107, 224 | hash: 225 | '0002fd94c689db923cba606efa53e1ecb8541bc2afa51793a2f7d5c4c6cda4937a3aa532096e15d1ba9109ee5553cda4985bdb0427bf2934b399c8b198cd72ab' 226 | */ 227 | return handleMessage(msg, ourKeypair) 228 | })) 229 | //const ts = Date.now() 230 | const msgMap = {} // deduplicate duplicate envelope timestamp 231 | for (const m of newMsgs) { 232 | //console.log('m', m) 233 | //console.log('ts', ts, '-', m.snodeExp) 234 | //const diff = ts - m.snodeExp 235 | //console.log(m.hash, 'expires in', diff.toLocaleString() + 'ms') 236 | // if diff < 0 then we can't use it... 237 | // or in the next poll time 238 | if (m.dataMessage) { 239 | //console.log('UPDATING lastHash to', m.hash) 240 | useLastHash = m.hash 241 | } 242 | msgMap[m.timestamp] = m 243 | } 244 | newMsgs = Object.values(msgMap) 245 | } 246 | //console.log('returning', useLastHash) 247 | return { 248 | lastHash: useLastHash, 249 | messages: newMsgs.filter(msg => !!msg) 250 | } 251 | } 252 | 253 | module.exports = { 254 | checkBox 255 | } 256 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/open_group_v2.js: -------------------------------------------------------------------------------- 1 | const lib = require('./lib.js') 2 | // eslint-disable-next-line camelcase 3 | const loki_crypto = require('./lib.loki_crypto.js') 4 | const protobuf = require('./protobuf.js') 5 | 6 | async function getToken(baseUrl, room, serverPubKeyHex, privKey, pubkeyHex) { 7 | const result = await lib.lsrpc(baseUrl, 'public_key=' + pubkeyHex, serverPubKeyHex, 8 | 'auth_token_challenge', 'GET', '', { Room: room }) 9 | //console.log('result', result) 10 | if (!result || !result.challenge || !result.challenge.ciphertext || !result.challenge.ephemeral_public_key) { 11 | console.error('open_group_v2::getToken - result', typeof (result), result) 12 | return 13 | } 14 | // decode everything into buffer format 15 | const ephermalPubBuf = Buffer.from(result.challenge.ephemeral_public_key, 'base64') 16 | const cipherTextBuf = Buffer.from(result.challenge.ciphertext, 'base64') 17 | 18 | const symmetricKey = loki_crypto.makeOnionSymKey(privKey, ephermalPubBuf) 19 | const tokenBuf = loki_crypto.decryptGCM(symmetricKey, cipherTextBuf) 20 | 21 | const tokenHex = tokenBuf.toString('hex') // fix up type 22 | 23 | const pkJSON = JSON.stringify({ public_key: pubkeyHex }) 24 | try { 25 | const activateRes = await lib.lsrpc(baseUrl, '', serverPubKeyHex, 26 | 'claim_auth_token', 'POST', pkJSON, { Room: room, Authorization: tokenHex }) 27 | // 28 | if (activateRes.status_code !== 200) { 29 | console.error('open_group_v2::getToken - claim_auth_token failure', activateRes) 30 | return 31 | } 32 | //console.log('activated', tokenHex) 33 | return tokenHex 34 | } catch (e) { 35 | console.error('open_group_v2::getToken - exception', e) 36 | } 37 | } 38 | 39 | // for multiple keypairs, servers and rooms 40 | class SessionOpenGroupV2Manager { 41 | constructor(options = {}) { 42 | this.servers = {} 43 | } 44 | 45 | joinServer(baseURL, serverPubkeyHex, options) { 46 | if (this.servers[baseURL]) { 47 | console.log('SessionOpenGroupV2Manager::joinServer - already joined', baseURL) 48 | return 49 | } 50 | this.servers[baseURL] = new SessionOpenGroupV2Server(baseURL, serverPubkeyHex, {...this.options, options }) 51 | } 52 | 53 | leaveServer(baseURL) { 54 | // this no server function for leaving? 55 | //this.rooms[room].stop() 56 | // leave all the rooms? not needed atm 57 | delete this.servers[baseURL] 58 | } 59 | 60 | joinServerRoom(baseURL, serverPubkeyHex, keypair, room, options) { 61 | if (this.servers[baseURL] === undefined) { 62 | this.joinServer(baseURL, serverPubkeyHex, options) 63 | } 64 | return this.servers[baseURL].joinRoom(keypair, room, options) 65 | } 66 | 67 | async getMessages() { 68 | const messages = await Promise.all(Object.values(this.servers).map(server => { 69 | return server.getMessages() 70 | })) 71 | return [].concat(...messages) 72 | } 73 | } 74 | 75 | class SessionOpenGroupV2Server { 76 | constructor(baseURL, serverPubkeyHex, manager, options = {}) { 77 | this.serverURL = baseURL 78 | this.serverPubkeyHex = serverPubkeyHex 79 | 80 | this.pollServer = false 81 | this.rooms = {} 82 | } 83 | 84 | _getKey(keypair, room) { 85 | return keypair.publicKeyHex + '_' + room 86 | } 87 | 88 | async joinRoom(keypair, room, options = {}) { 89 | const key = this._getKey(keypair, room) 90 | if (this.rooms[key]) { 91 | console.log('SessionOpenGroupV2Server::joinRoom', keypair.publicKeyHex, 'already joined', room) 92 | return 93 | } 94 | this.rooms[key] = new SessionOpenGroupV2Room(this, keypair, room, options) 95 | await this.rooms[key].subscribe() 96 | return this.rooms[key].token ? this.rooms[key] : false 97 | } 98 | 99 | async leaveRoom(keypair, room) { 100 | const key = this._getKey(keypair, room) 101 | // this no server function for leaving? 102 | await this.rooms[room].unsubscribe() 103 | delete this.rooms[key] 104 | } 105 | 106 | async getMessages() { 107 | try { 108 | const requests = [] 109 | const tokenLookup = {} 110 | for (const id in this.rooms) { 111 | const room = this.rooms[id] 112 | await room.ensureToken() 113 | if (!room.token) { 114 | console.warn('SessionOpenGroupV2Server::getMessages - ', room.room, 'no token (yet?) to poll with') 115 | continue 116 | } 117 | tokenLookup[room.token] = room 118 | requests.push({ 119 | room_id: room.room, 120 | auth_token: room.token, 121 | from_message_server_id: room.lastId, 122 | from_deletion_server_id: 0, 123 | }) 124 | } 125 | // , 'using', this.token 126 | //console.log('getting from lastId', room.lastId) 127 | //console.log('requests', requests) 128 | const result = await lib.lsrpc(this.serverURL, '', this.serverPubkeyHex, 129 | 'compact_poll', 'POST', JSON.stringify({ requests }), {}) 130 | //console.log('result', result) 131 | if (result.status_code !== 200) { 132 | console.error('SessionOpenGroupV2Server::getMessages - non-200 response', result) 133 | return null 134 | } 135 | /* 136 | if (result.status_code === 401) { 137 | this.token = false 138 | console.log('refreshing token') 139 | this.token = getToken(this.serverUrl, this.room, this.serverPubkeyHex, this.keypair.privKey, this.keypair.pubKey.toString('hex')) 140 | return null 141 | } 142 | */ 143 | if (!result || !result.results || !result.results.length) { 144 | console.warn('SessionOpenGroupV2Server::getMessages - ', this.room, 'no result', result) 145 | return null 146 | } 147 | 148 | const msgs = [] 149 | for (const id in requests) { 150 | const request = requests[id] 151 | const room = tokenLookup[request.auth_token] 152 | let last = room.lastId 153 | // if this is always sorted asc, can just grab the last message id 154 | for (const i in result.results[id].messages) { 155 | // server_id, public_key, timestamp, data, signature 156 | const serverMsg = result.results[id].messages[i] 157 | const humanId = serverMsg.server_id + ' from ' + serverMsg.public_key + ' in ' + room.room + ' on ' + this.serverURL 158 | // get content 159 | const contentBuf = Buffer.from(serverMsg.data, 'base64') 160 | const content = protobuf.decodeContentMessage(contentBuf, humanId) 161 | // check sig 162 | let verified = false 163 | try { 164 | verified = loki_crypto.verifySigDataV2( 165 | Buffer.from(serverMsg.public_key, 'hex'), 166 | contentBuf, 167 | Buffer.from(serverMsg.signature, 'base64') 168 | ) 169 | } catch (e) { 170 | console.warn('SessionOpenGroupV2Server::getMessages - Could not verify signature on', humanId) 171 | } 172 | 173 | // make sure message is decoded 174 | // discard messages on first poll 175 | // and not a message we send 176 | const notUs = room.keypair.publicKeyHex !== serverMsg.public_key 177 | if (content && room.lastId && notUs) { 178 | //console.log('data', data) 179 | const message = { 180 | // identity 181 | serverURL: this.serverURL, 182 | room: room.room, 183 | id: serverMsg.server_id, 184 | source: serverMsg.public_key, 185 | roomHandle: room, 186 | // message 187 | destination: room.keypair.publicKeyHex, 188 | // unique to the intended message 189 | timestamp: serverMsg.timestamp, 190 | // specific to the transport 191 | existedBy: serverMsg.timestamp, 192 | // access to quotes/attachments/etc 193 | content, 194 | verified, 195 | } 196 | // if dataMessage, unpack it a bit 197 | if (content.dataMessage) { 198 | // unique to the intended message 199 | if (content.dataMessage.timestamp) message.timestamp = content.dataMessage.timestamp 200 | message.body = content.dataMessage.body 201 | if (content.dataMessage.profile) { 202 | message.profile = { 203 | displayName: content.dataMessage.profile.displayName, 204 | avatar: { 205 | url: content.dataMessage.profile.profilePicture, 206 | key: content.dataMessage.profile.profileKey, 207 | } 208 | } 209 | } 210 | // FIXME: quote, attachments? 211 | } 212 | msgs.push(message) 213 | } 214 | last = Math.max(last, serverMsg.server_id) 215 | } 216 | room.lastId = last 217 | } 218 | return msgs 219 | } catch (e) { 220 | console.error('SessionOpenGroupV2Server::getMessages - Getting messages error', e) 221 | } 222 | return null 223 | } 224 | } 225 | 226 | class SessionOpenGroupV2Room { 227 | constructor(server, keypair, room, options = {}) { 228 | this.server = server 229 | this.keypair = keypair 230 | this.room = room 231 | 232 | this.lastId = options.lastId || 0 233 | this.token = options.token || '' 234 | } 235 | 236 | async ensureToken() { 237 | if (!this.token) { 238 | this.token = await getToken(this.server.serverURL, this.room, this.server.serverPubkeyHex, this.keypair.privKey, this.keypair.publicKeyHex) 239 | } 240 | } 241 | 242 | async subscribe() { 243 | // this adds us to the count 244 | await this.ensureToken() 245 | if (!this.token) { 246 | console.log('SessionOpenGroupV2Room::subscribe - Can not subscribe no token') 247 | return 248 | } 249 | const lastMessageRes = await lib.lsrpc(this.server.serverURL, 'limit=1', this.server.serverPubkeyHex, 250 | 'messages', 'GET', '', { Room: this.room, Authorization: this.token }) 251 | if (!lastMessageRes || !lastMessageRes.messages || !lastMessageRes.messages.length) { 252 | console.error('SessionOpenGroupV2Server::subscribe - no room messages', lastMessageRes) 253 | return 254 | } 255 | if (lastMessageRes.status_code !== 200) { 256 | console.error('SessionOpenGroupV2Server::subscribe - non-200 response', lastMessageRes) 257 | return 258 | } 259 | this.lastId = lastMessageRes.messages[0].server_id 260 | //console.log('setting', this.room + '@' + this.server.serverURL, 'last message to', this.lastId) 261 | } 262 | 263 | async unsubscribe() { 264 | // DELETE token 265 | } 266 | 267 | async send(text, options = {}) { 268 | try { 269 | // we need to pad text 270 | const ts = Date.now() 271 | 272 | // padPlainTextBuffer returns uint8 array 273 | const plaintextBuf = Buffer.from(protobuf.encodeContentMessage(text, ts, options)) 274 | const message = { 275 | // sender 276 | public_key: this.keypair.publicKeyHex, 277 | timestamp: ts, 278 | data: plaintextBuf.toString('base64'), // base64 encode plaintextBuf 279 | signature: loki_crypto.getSigDataV2(this.keypair.privKey, plaintextBuf), //base64 280 | } 281 | const result = await lib.lsrpc(this.server.serverURL, '', this.server.serverPubkeyHex, 282 | 'messages', 'POST', JSON.stringify(message), { Room: this.room, Authorization: this.token }) 283 | // result.message: server_id, public_key, timestamp, data, signature 284 | if (!result || !result.message || !result.message.server_id) { 285 | console.error('SessionOpenGroupV2Room::send - bad result?', result) 286 | return false 287 | } 288 | return result.message.server_id 289 | } catch (e) { 290 | console.error('SessionOpenGroupV2Room::send - Sending messages error', e) 291 | } 292 | return null 293 | } 294 | 295 | async messageDelete(messageId) { 296 | try { 297 | const messageDeleteResult = await lib.lsrpc(this.server.serverURL, '', this.server.serverPubkeyHex, 298 | 'messages/' + messageId, 'DELETE', '', { Authorization: this.token, Room: this.room}) 299 | console.log('messageDeleteResult', messageDeleteResult) 300 | if (messageDeleteResult.status_code !== 200) { 301 | console.error('SessionOpenGroupV2Room::delete - Wrong response received', messageDeleteResult) 302 | return false 303 | } 304 | return true 305 | } catch (e) { 306 | console.error('SessionOpenGroupV2Room::delete - Getting messages error', e) 307 | } 308 | return null 309 | } 310 | } 311 | 312 | module.exports = { 313 | SessionOpenGroupV2Manager: new SessionOpenGroupV2Manager(), 314 | } 315 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p 2 | 3 | 4 | 5 | JSDoc: Module: session-client 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Module: session-client

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 | 41 |
Creates a new Session client
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
Properties:
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |
NameTypeDescription
pollRate 91 | 92 | 93 | Number 94 | 95 | 96 | 97 | How much delay between poll requests
lastHash 114 | 115 | 116 | Number 117 | 118 | 119 | 120 | Poll for messages from this hash on
displayName 137 | 138 | 139 | String 140 | 141 | 142 | 143 | Send messages with this profile name
homeServer 160 | 161 | 162 | String 163 | 164 | 165 | 166 | URL for this identity's file server
homeServerPubKey 183 | 184 | 185 | String 186 | 187 | 188 | 189 | Pubkey in hex for this identity's file server
identityOutput 206 | 207 | 208 | String 209 | 210 | 211 | 212 | human readable string with seed words if generated a new identity
ourPubkeyHex 229 | 230 | 231 | String 232 | 233 | 234 | 235 | This identity's pubkey (SessionID)
keypair 252 | 253 | 254 | object 255 | 256 | 257 | 258 | This identity's keypair buffers
open 275 | 276 | 277 | Boolean 278 | 279 | 280 | 281 | Should we continue polling for messages
encAvatarUrl 298 | 299 | 300 | String 301 | 302 | 303 | 304 | Encrypted avatar URL
profileKeyBuf 321 | 322 | 323 | Buffer 324 | 325 | 326 | 327 | Key to decrypt avatar URL
339 | 340 | 341 | 342 | 343 |
344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 |
Implements:
357 |
    358 | 359 |
  • EventEmitter
  • 360 | 361 |
362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 |
Author:
370 |
371 |
    372 |
  • Ryan Tharp
  • 373 |
374 |
375 | 376 | 377 | 378 | 379 | 380 |
License:
381 |
  • ISC
382 | 383 | 384 | 385 | 386 | 387 |
Source:
388 |
391 | 392 | 393 | 394 |
Tutorials:
395 |
396 |
    397 |
  • Tutorial: sample.js
  • 398 |
399 |
400 | 401 | 402 | 403 | 404 | 405 |
406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 |
429 | 430 | 431 | 432 | 433 | 434 | 435 |

Classes

436 | 437 |
438 |
SessionClient
439 |
440 |
441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 |

Type Definitions

455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 |

messagesCallback(messages)

463 | 464 | 465 | 466 | 467 | 468 | 469 |
470 | content dataMessage protobuf 471 |
472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 |
Parameters:
482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 |
NameTypeDescription
messages 510 | 511 | 512 | Array 513 | 514 | 515 | 516 | an array of Content protobuf
528 | 529 | 530 | 531 | 532 | 533 | 534 |
535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 |
Source:
562 |
565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 |
573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 |

updateLastHashCallback(hash)

600 | 601 | 602 | 603 | 604 | 605 | 606 |
607 | Handle when the cursor in the pubkey's inbox moves 608 |
609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 |
Parameters:
619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 |
NameTypeDescription
hash 647 | 648 | 649 | String 650 | 651 | 652 | 653 | The last hash returns from the storage server for this pubkey
665 | 666 | 667 | 668 | 669 | 670 | 671 |
672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 |
Source:
699 |
702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 |
710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 |
735 | 736 |
737 | 738 | 739 | 740 | 741 |
742 | 743 | 746 | 747 |
748 | 749 |
750 | Documentation generated by JSDoc 3.6.6 on Sat Jan 21 2023 00:39:15 GMT+0000 (Coordinated Universal Time) 751 |
752 | 753 | 754 | 755 | 756 | -------------------------------------------------------------------------------- /external/protos/SignalService.proto: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/signalapp/libsignal-service-java/blob/4684a49b2ed8f32be619e0d0eea423626b6cb2cb/protobuf/SignalService.proto 2 | package signalservice; 3 | 4 | option java_package = "org.whispersystems.signalservice.internal.push"; 5 | option java_outer_classname = "SignalServiceProtos"; 6 | 7 | message Envelope { 8 | enum Type { 9 | UNKNOWN = 0; 10 | CIPHERTEXT = 1; 11 | KEY_EXCHANGE = 2; 12 | PREKEY_BUNDLE = 3; //This field is used by Signal. DO NOT TOUCH!. 13 | RECEIPT = 5; 14 | UNIDENTIFIED_SENDER = 6; 15 | CLOSED_GROUP_CIPHERTEXT = 7; 16 | FALLBACK_MESSAGE = 101; // Custom Encryption for when we don't have a session or we need to establish a session 17 | } 18 | // @required 19 | required Type type = 1; 20 | optional string source = 2; 21 | optional uint32 sourceDevice = 7; 22 | optional string relay = 3; 23 | // @required 24 | required uint64 timestamp = 5; 25 | optional bytes legacyMessage = 6; // Contains an encrypted DataMessage 26 | optional bytes content = 8; // Contains an encrypted Content 27 | optional string serverGuid = 9; 28 | optional uint64 serverTimestamp = 10; 29 | 30 | } 31 | 32 | message Content { 33 | optional DataMessage dataMessage = 1; 34 | optional SyncMessage syncMessage = 2; 35 | optional CallMessage callMessage = 3; 36 | optional NullMessage nullMessage = 4; 37 | optional ReceiptMessage receiptMessage = 5; 38 | optional TypingMessage typingMessage = 6; 39 | optional ConfigurationMessage configurationMessage = 7; 40 | optional DataExtractionNotification dataExtractionNotification = 8; 41 | optional Unsend unsendMessage = 9; 42 | optional MessageRequestResponse messageRequestResponse = 10; 43 | optional PreKeyBundleMessage preKeyBundleMessage = 101; // The presence of this indicated that we want to establish a new session (Session Request) 44 | optional LokiAddressMessage lokiAddressMessage = 102; 45 | optional PairingAuthorisationMessage pairingAuthorisation = 103; 46 | } 47 | 48 | message MediumGroupCiphertext { 49 | optional bytes ciphertext = 1; 50 | optional bytes source = 2; 51 | optional uint32 keyIdx = 3; 52 | } 53 | 54 | message MediumGroupContent { 55 | optional bytes ciphertext = 1; 56 | optional bytes ephemeralKey = 2; 57 | } 58 | 59 | message ClosedGroupUpdateV2 { 60 | 61 | enum Type { 62 | NEW = 1; // publicKey, name, encryptionKeyPair, members, admins 63 | UPDATE = 2; // name, members 64 | ENCRYPTION_KEY_PAIR = 3; // wrappers 65 | } 66 | 67 | message KeyPair { 68 | // @required 69 | required bytes publicKey = 1; 70 | // @required 71 | required bytes privateKey = 2; 72 | } 73 | 74 | message KeyPairWrapper { 75 | // @required 76 | required bytes publicKey = 1; // The public key of the user the key pair is meant for 77 | // @required 78 | required bytes encryptedKeyPair = 2; // The encrypted key pair 79 | } 80 | 81 | // @required 82 | required Type type = 1; 83 | optional bytes publicKey = 2; 84 | optional string name = 3; 85 | optional KeyPair encryptionKeyPair = 4; 86 | repeated bytes members = 5; 87 | repeated bytes admins = 6; 88 | repeated KeyPairWrapper wrappers = 7; 89 | } 90 | 91 | message MediumGroupUpdate { 92 | enum Type { 93 | NEW = 0; // groupPublicKey, name, senderKeys, members, admins, groupPrivateKey 94 | INFO = 1; // groupPublicKey, name, senderKeys, members, admins 95 | SENDER_KEY_REQUEST = 2; // groupPublicKey 96 | SENDER_KEY = 3; // groupPublicKey, senderKeys 97 | } 98 | 99 | message SenderKey { 100 | optional bytes chainKey = 1; 101 | optional uint32 keyIndex = 2; 102 | optional bytes publicKey = 3; 103 | } 104 | 105 | optional string name = 1; 106 | optional bytes groupPublicKey = 2; 107 | optional bytes groupPrivateKey = 3; 108 | repeated SenderKey senderKeys = 4; 109 | repeated bytes members = 5; 110 | repeated bytes admins = 6; 111 | optional Type type = 7; 112 | } 113 | 114 | message LokiAddressMessage { 115 | enum Type { 116 | HOST_REACHABLE = 0; 117 | HOST_UNREACHABLE = 1; 118 | } 119 | optional string p2pAddress = 1; 120 | optional uint32 p2pPort = 2; 121 | optional Type type = 3; 122 | } 123 | 124 | message PairingAuthorisationMessage { 125 | optional string primaryDevicePubKey = 1; 126 | optional string secondaryDevicePubKey = 2; 127 | optional bytes requestSignature = 3; 128 | optional bytes grantSignature = 4; 129 | } 130 | 131 | message PreKeyBundleMessage { 132 | optional bytes identityKey = 1; 133 | optional uint32 deviceId = 2; 134 | optional uint32 preKeyId = 3; 135 | optional uint32 signedKeyId = 4; 136 | optional bytes preKey = 5; 137 | optional bytes signedKey = 6; 138 | optional bytes signature = 7; 139 | } 140 | 141 | message CallMessage { 142 | 143 | enum Type { 144 | PRE_OFFER = 6; 145 | OFFER = 1; 146 | ANSWER = 2; 147 | PROVISIONAL_ANSWER = 3; 148 | ICE_CANDIDATES = 4; 149 | END_CALL = 5; 150 | } 151 | 152 | // @required 153 | required Type type = 1; 154 | repeated string sdps = 2; 155 | repeated uint32 sdpMLineIndexes = 3; 156 | repeated string sdpMids = 4; 157 | 158 | // @required 159 | required string uuid = 5; 160 | } 161 | 162 | message ConfigurationMessage { 163 | 164 | message ClosedGroup { 165 | optional bytes publicKey = 1; 166 | optional string name = 2; 167 | optional KeyPair encryptionKeyPair = 3; 168 | repeated bytes members = 4; 169 | repeated bytes admins = 5; 170 | } 171 | 172 | message Contact { 173 | // @required 174 | required bytes publicKey = 1; 175 | // @required 176 | required string name = 2; 177 | optional string profilePicture = 3; 178 | optional bytes profileKey = 4; 179 | optional bool isApproved = 5; 180 | optional bool isBlocked = 6; 181 | optional bool didApproveMe = 7; 182 | } 183 | 184 | repeated ClosedGroup closedGroups = 1; 185 | repeated string openGroups = 2; 186 | optional string displayName = 3; 187 | optional string profilePicture = 4; 188 | optional bytes profileKey = 5; 189 | repeated Contact contacts = 6; 190 | } 191 | 192 | message DataExtractionNotification { 193 | 194 | enum Type { 195 | SCREENSHOT = 1; // no way to know this on Desktop 196 | MEDIA_SAVED = 2; // timestamp 197 | } 198 | 199 | // @required 200 | required Type type = 1; 201 | optional uint64 timestamp = 2; 202 | } 203 | 204 | message DataMessage { 205 | enum Flags { 206 | END_SESSION = 1; 207 | EXPIRATION_TIMER_UPDATE = 2; 208 | PROFILE_KEY_UPDATE = 4; 209 | SESSION_RESTORE = 64; 210 | UNPAIRING_REQUEST = 128; 211 | } 212 | 213 | message Reaction { 214 | enum Action { 215 | REACT = 0; 216 | REMOVE = 1; 217 | } 218 | // @required 219 | required uint64 id = 1; // Message timestamp 220 | // @required 221 | required string author = 2; 222 | optional string emoji = 3; 223 | // @required 224 | required Action action = 4; 225 | } 226 | 227 | message Quote { 228 | message QuotedAttachment { 229 | optional string contentType = 1; 230 | optional string fileName = 2; 231 | optional AttachmentPointer thumbnail = 3; 232 | } 233 | 234 | // @required 235 | optional uint64 id = 1; 236 | // @required 237 | optional string author = 2; 238 | optional string text = 3; 239 | repeated QuotedAttachment attachments = 4; 240 | } 241 | 242 | message Contact { 243 | message Name { 244 | optional string givenName = 1; 245 | optional string familyName = 2; 246 | optional string prefix = 3; 247 | optional string suffix = 4; 248 | optional string middleName = 5; 249 | optional string displayName = 6; 250 | } 251 | 252 | message Phone { 253 | enum Type { 254 | HOME = 1; 255 | MOBILE = 2; 256 | WORK = 3; 257 | CUSTOM = 4; 258 | } 259 | 260 | optional string value = 1; 261 | optional Type type = 2; 262 | optional string label = 3; 263 | } 264 | 265 | message Email { 266 | enum Type { 267 | HOME = 1; 268 | MOBILE = 2; 269 | WORK = 3; 270 | CUSTOM = 4; 271 | } 272 | 273 | optional string value = 1; 274 | optional Type type = 2; 275 | optional string label = 3; 276 | } 277 | 278 | message PostalAddress { 279 | enum Type { 280 | HOME = 1; 281 | WORK = 2; 282 | CUSTOM = 3; 283 | } 284 | 285 | optional Type type = 1; 286 | optional string label = 2; 287 | optional string street = 3; 288 | optional string pobox = 4; 289 | optional string neighborhood = 5; 290 | optional string city = 6; 291 | optional string region = 7; 292 | optional string postcode = 8; 293 | optional string country = 9; 294 | } 295 | 296 | message Avatar { 297 | optional AttachmentPointer avatar = 1; 298 | optional bool isProfile = 2; 299 | } 300 | 301 | optional Name name = 1; 302 | repeated Phone number = 3; 303 | repeated Email email = 4; 304 | repeated PostalAddress address = 5; 305 | optional Avatar avatar = 6; 306 | optional string organization = 7; 307 | } 308 | 309 | message Preview { 310 | // @required 311 | optional string url = 1; 312 | optional string title = 2; 313 | optional AttachmentPointer image = 3; 314 | } 315 | 316 | // Loki: A custom message for our profile 317 | message LokiProfile { 318 | optional string displayName = 1; 319 | optional string avatar = 2; 320 | } 321 | 322 | message OpenGroupInvitation { 323 | // @required 324 | required string url = 1; 325 | // @required 326 | required string name = 3; 327 | } 328 | 329 | message GroupInvitation { 330 | optional string serverAddress = 1; 331 | optional uint32 channelId = 2; 332 | optional string serverName = 3; 333 | } 334 | 335 | optional string body = 1; 336 | repeated AttachmentPointer attachments = 2; 337 | optional GroupContext group = 3; 338 | optional uint32 flags = 4; 339 | optional uint32 expireTimer = 5; 340 | optional bytes profileKey = 6; 341 | optional uint64 timestamp = 7; 342 | optional Quote quote = 8; 343 | repeated Contact contact = 9; 344 | repeated Preview preview = 10; 345 | optional Reaction reaction = 11; 346 | optional LokiProfile profile = 101; // Loki: The profile of the current user 347 | optional GroupInvitation groupInvitation = 102; // Loki: Invitation to a public chat 348 | optional MediumGroupUpdate mediumGroupUpdate = 103; // Loki 349 | optional ClosedGroupUpdateV2 closedGroupUpdateV2 = 104; // Loki 350 | optional string syncTarget = 105; 351 | } 352 | 353 | message NullMessage { 354 | optional bytes padding = 1; 355 | } 356 | 357 | message ReceiptMessage { 358 | enum Type { 359 | DELIVERY = 0; 360 | READ = 1; 361 | } 362 | 363 | // @required 364 | required Type type = 1; 365 | repeated uint64 timestamp = 2; 366 | } 367 | 368 | message TypingMessage { 369 | enum Action { 370 | STARTED = 0; 371 | STOPPED = 1; 372 | } 373 | // @required 374 | required uint64 timestamp = 1; 375 | // @required 376 | required Action action = 2; 377 | optional bytes groupId = 3; 378 | } 379 | 380 | message Unsend { 381 | // @required 382 | required uint64 timestamp = 1; 383 | // @required 384 | required string author = 2; 385 | } 386 | 387 | message MessageRequestResponse { 388 | // @required 389 | required bool isApproved = 1; 390 | optional bytes profileKey = 2; 391 | optional DataMessage.LokiProfile profile = 3; 392 | } 393 | 394 | message Verified { 395 | enum State { 396 | DEFAULT = 0; 397 | VERIFIED = 1; 398 | UNVERIFIED = 2; 399 | } 400 | 401 | optional string destination = 1; 402 | optional bytes identityKey = 2; 403 | optional State state = 3; 404 | optional bytes nullMessage = 4; 405 | } 406 | 407 | message SyncMessage { 408 | message Sent { 409 | message UnidentifiedDeliveryStatus { 410 | optional string destination = 1; 411 | optional bool unidentified = 2; 412 | } 413 | 414 | optional string destination = 1; 415 | optional uint64 timestamp = 2; 416 | optional DataMessage message = 3; 417 | optional uint64 expirationStartTimestamp = 4; 418 | repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5; 419 | } 420 | 421 | message Contacts { 422 | optional AttachmentPointer blob = 1; 423 | optional bool complete = 2 [default = false]; 424 | optional bytes data = 101; 425 | } 426 | 427 | message Groups { 428 | optional AttachmentPointer blob = 1; 429 | optional bytes data = 101; 430 | } 431 | 432 | message Blocked { 433 | repeated string numbers = 1; 434 | repeated bytes groupIds = 2; 435 | } 436 | 437 | message Request { 438 | enum Type { 439 | UNKNOWN = 0; 440 | CONTACTS = 1; 441 | GROUPS = 2; 442 | BLOCKED = 3; 443 | CONFIGURATION = 4; 444 | } 445 | 446 | optional Type type = 1; 447 | } 448 | 449 | message Read { 450 | optional string sender = 1; 451 | optional uint64 timestamp = 2; 452 | } 453 | 454 | message Configuration { 455 | optional bool readReceipts = 1; 456 | optional bool unidentifiedDeliveryIndicators = 2; 457 | optional bool typingIndicators = 3; 458 | optional bool linkPreviews = 4; 459 | } 460 | 461 | message OpenGroupDetails { 462 | optional string url = 1; 463 | optional uint32 channelId = 2; 464 | } 465 | 466 | optional Sent sent = 1; 467 | optional Contacts contacts = 2; 468 | optional Groups groups = 3; 469 | optional Request request = 4; 470 | repeated Read read = 5; 471 | optional Blocked blocked = 6; 472 | optional Verified verified = 7; 473 | optional Configuration configuration = 9; 474 | optional bytes padding = 8; 475 | repeated OpenGroupDetails openGroups = 100; 476 | } 477 | 478 | message AttachmentPointer { 479 | enum Flags { 480 | VOICE_MESSAGE = 1; 481 | } 482 | 483 | // @required 484 | required fixed64 id = 1; 485 | optional string contentType = 2; 486 | optional bytes key = 3; 487 | optional uint32 size = 4; 488 | optional bytes thumbnail = 5; 489 | optional bytes digest = 6; 490 | optional string fileName = 7; 491 | optional uint32 flags = 8; 492 | optional uint32 width = 9; 493 | optional uint32 height = 10; 494 | optional string caption = 11; 495 | optional string url = 101; 496 | } 497 | 498 | message GroupContext { 499 | enum Type { 500 | UNKNOWN = 0; 501 | UPDATE = 1; 502 | DELIVER = 2; 503 | QUIT = 3; 504 | REQUEST_INFO = 4; 505 | } 506 | // @required 507 | optional bytes id = 1; 508 | // @required 509 | optional Type type = 2; 510 | optional string name = 3; 511 | repeated string members = 4; 512 | optional AttachmentPointer avatar = 5; 513 | repeated string admins = 6; 514 | } 515 | 516 | message ContactDetails { 517 | message Avatar { 518 | optional string contentType = 1; 519 | optional uint32 length = 2; 520 | } 521 | 522 | optional string number = 1; 523 | optional string name = 2; 524 | optional Avatar avatar = 3; 525 | optional string color = 4; 526 | optional Verified verified = 5; 527 | optional bytes profileKey = 6; 528 | optional bool blocked = 7; 529 | optional uint32 expireTimer = 8; 530 | optional string nickname = 101; 531 | } 532 | 533 | message GroupDetails { 534 | message Avatar { 535 | optional string contentType = 1; 536 | optional uint32 length = 2; 537 | } 538 | 539 | optional bytes id = 1; 540 | optional string name = 2; 541 | repeated string members = 3; 542 | optional Avatar avatar = 4; 543 | optional bool active = 5 [default = true]; 544 | optional uint32 expireTimer = 6; 545 | optional string color = 7; 546 | optional bool blocked = 8; 547 | repeated string admins = 9; 548 | optional bool is_medium_group = 10; 549 | } -------------------------------------------------------------------------------- /lib/lib.loki_crypto.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const libsignal = require('libsignal') 3 | const bb = require('bytebuffer') 4 | const _sodium = require('libsodium-wrappers-sumo') // maybe put in session-client? 5 | const binary = require('./lib.binary.js') 6 | 7 | /* 8 | bufferFrom64 9 | bufferTo64 10 | bufferFromHex 11 | bufferToHex 12 | */ 13 | 14 | const IV_LENGTH = 16 15 | const NONCE_LENGTH = 12 16 | const TAG_LENGTH = 16 17 | 18 | async function DHEncrypt(symmetricKey, plainText) { 19 | const iv = crypto.randomBytes(IV_LENGTH) 20 | // DH 21 | const ciphertext = await libsignal.crypto.encrypt( 22 | symmetricKey, 23 | plainText, 24 | iv 25 | ) 26 | const ivAndCiphertext = new Uint8Array( 27 | iv.byteLength + ciphertext.byteLength 28 | ) 29 | ivAndCiphertext.set(new Uint8Array(iv)) 30 | ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength) 31 | return ivAndCiphertext 32 | } 33 | 34 | async function DHDecrypt(symmetricKey, ivAndCiphertext) { 35 | const iv = ivAndCiphertext.slice(0, IV_LENGTH) 36 | const ciphertext = ivAndCiphertext.slice(IV_LENGTH) 37 | // DH 38 | return libsignal.crypto.decrypt(symmetricKey, ciphertext, iv) 39 | } 40 | 41 | // used for proxy requests 42 | const DHEncrypt64 = async (symmetricKey, plainText) => { 43 | const ivAndCiphertext = await DHEncrypt(symmetricKey, plainText) 44 | return bb.wrap(ivAndCiphertext).toString('base64') 45 | } 46 | 47 | // used for tokens 48 | const DHDecrypt64 = async (symmetricKey, cipherText64) => { 49 | // base64 decode 50 | const ivAndCiphertext = Buffer.from( 51 | bb.wrap(cipherText64, 'base64').toArrayBuffer() 52 | ) 53 | return DHDecrypt(symmetricKey, ivAndCiphertext) 54 | } 55 | 56 | function generateEphemeralKeyPair() { 57 | // generate a x25519 keypair 58 | const keys = libsignal.curve.generateKeyPair() 59 | // Signal protocol prepends with "0x05" 60 | keys.pubKey = keys.pubKey.slice(1) 61 | return keys 62 | } 63 | 64 | function makeSymmetricKey(privKeyBuf, pubKeyBuf) { 65 | if (pubKeyBuf.byteLength === 32) { 66 | pubKeyBuf = Buffer.concat([Buffer.from('05', 'hex'), pubKeyBuf]) 67 | } 68 | // is this a promise? no (needs .async. for promise) 69 | const symmetricKey = libsignal.curve.calculateAgreement( 70 | pubKeyBuf, 71 | privKeyBuf 72 | ) 73 | //console.log('symmetricKey', symmetricKey) 74 | return symmetricKey 75 | } 76 | 77 | function makeOnionSymKey(privKeyBuf, pubKeyBuf) { 78 | if (pubKeyBuf.byteLength === 32) { 79 | pubKeyBuf = Buffer.concat([Buffer.from('05', 'hex'), pubKeyBuf]) 80 | } 81 | // symKey 82 | const keyAgreement = libsignal.curve.calculateAgreement( 83 | pubKeyBuf, 84 | privKeyBuf 85 | ) 86 | //console_wrapper.log('makeOnionSymKey agreement', keyAgreement.toString('hex')) 87 | 88 | // hash the key agreement 89 | const hashedSymmetricKeyBuf = crypto.createHmac('sha256', 'LOKI').update(keyAgreement).digest() 90 | 91 | return hashedSymmetricKeyBuf 92 | } 93 | 94 | function encryptGCM(symmetricKey, plaintextEnc) { 95 | // not on the node side 96 | const nonce = crypto.randomBytes(NONCE_LENGTH) // Buffer (object) 97 | 98 | const cipher = crypto.createCipheriv('aes-256-gcm', symmetricKey, nonce) 99 | const ciphertext = Buffer.concat([cipher.update(plaintextEnc), cipher.final()]) 100 | const tag = cipher.getAuthTag() 101 | 102 | const finalBuf = Buffer.concat([nonce, ciphertext, tag]) 103 | return finalBuf 104 | } 105 | 106 | // used for avatar download 107 | function decryptGCM(symmetricKey, ivCiphertextAndTag) { 108 | const nonce = ivCiphertextAndTag.slice(0, NONCE_LENGTH) 109 | const ciphertext = ivCiphertextAndTag.slice(NONCE_LENGTH, ivCiphertextAndTag.byteLength - TAG_LENGTH) 110 | const tag = ivCiphertextAndTag.slice(ivCiphertextAndTag.byteLength - TAG_LENGTH) 111 | 112 | const decipher = crypto.createDecipheriv('aes-256-gcm', symmetricKey, nonce) 113 | decipher.setAuthTag(tag) 114 | //return decipher.update(ciphertext, 'binary', 'utf8') + decipher.final(); 115 | return Buffer.concat([decipher.update(ciphertext), decipher.final()]) 116 | } 117 | 118 | // for attachments 119 | async function encryptCBC(keysBuf, plaintextEnc) { 120 | if (plaintextEnc === undefined) { 121 | console.trace('lib.loki_crypo::encryptCBC - passed undefined plaintextEnc') 122 | return 123 | } 124 | const aesKey = keysBuf.slice(0, 32) 125 | const macKey = keysBuf.slice(32, 64) 126 | 127 | // not on the node side 128 | const iv = crypto.randomBytes(IV_LENGTH) // Buffer (object) 129 | const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv) // Cipheriv object 130 | const ciphertext = Buffer.concat([cipher.update(plaintextEnc), cipher.final()]) 131 | const ivAndCiphertext = Buffer.concat([iv, ciphertext]) 132 | // generate mac 133 | const macBuf = crypto.createHmac('sha256', macKey).update(ivAndCiphertext).digest() 134 | const finalBuf = Buffer.concat([ivAndCiphertext, macBuf]) 135 | return finalBuf 136 | } 137 | 138 | function decryptCBC(keysBuf, ivCiphertextAndMac, remoteDigest) { 139 | const aesKey = keysBuf.slice(0, 32) 140 | const iv = ivCiphertextAndMac.slice(0, IV_LENGTH) 141 | const ciphertext = ivCiphertextAndMac.slice(IV_LENGTH, ivCiphertextAndMac.byteLength - 32) 142 | // FIXME: implement mac and digest checking 143 | // const mac = ivCiphertextAndMac.slice(ivCiphertextAndMac.byteLength - 32); 144 | if (remoteDigest) { 145 | // digest checking will need a digest passed in to compare... 146 | // or we need to export ivCiphertextAndMac 147 | const localDigest = crypto.createHash('sha256').update(ivCiphertextAndMac).digest() 148 | if (Buffer.compare(localDigest, remoteDigest)) { 149 | // mismatch, what do? 150 | } 151 | } 152 | const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv) 153 | return Buffer.concat([decipher.update(ciphertext), decipher.final()]) 154 | } 155 | 156 | // FIXME: bring in the multidevice support functions 157 | // or maybe put them into a separate libraries 158 | // marking async, because we likely will need some IO in the future 159 | // reply_to if 0 not, is also required in adnMessage 160 | async function getSigData(sigVer, privKey, noteValue, adnMessage) { 161 | let sigString = '' 162 | sigString += adnMessage.text.trim() 163 | sigString += noteValue.timestamp 164 | if (noteValue.quote) { 165 | sigString += noteValue.quote.id 166 | sigString += noteValue.quote.author 167 | sigString += noteValue.quote.text.trim() 168 | if (adnMessage.reply_to) { 169 | sigString += adnMessage.reply_to 170 | } 171 | } 172 | /* 173 | sigString += [...attachmentAnnotations, ...previewAnnotations] 174 | .map(data => data.id || data.image.id) 175 | .sort() 176 | .join(); 177 | */ 178 | sigString += sigVer 179 | const sigData = Buffer.from(bb.wrap(sigString, 'utf8').toArrayBuffer()) 180 | // symKey 181 | const sig = await libsignal.curve.calculateSignature(privKey, sigData) 182 | // const sig = makeSymmetricKey(privKey, sigData) 183 | return sig.toString('hex') 184 | } 185 | 186 | function verifySigDataV2(pubKeyBuf, messageBuf, sigBuf) { 187 | return libsignal.curve.verifySignature(pubKeyBuf, messageBuf, sigBuf) 188 | } 189 | 190 | function getSigDataV2(privKey, messageBuf) { 191 | return libsignal.curve.calculateSignature( 192 | privKey, 193 | messageBuf 194 | ).toString('base64') 195 | } 196 | 197 | const sha512Multipart = async parts => { 198 | await _sodium.ready 199 | const sodium = _sodium 200 | return sodium.crypto_hash_sha512(binary.concatUInt8Array(...parts)) 201 | } 202 | 203 | /** 204 | * 205 | * @param messageParts concatenated byte array 206 | * @param ourKeyPair our devices keypair 207 | * @param ka blinded secret key for this open group 208 | * @param kA blinded pubkey for this open group 209 | * @returns blinded signature 210 | */ 211 | async function blindedED25519Signature(messageParts, ourKeyPair, ka, kA) { 212 | //const sodium = await getSodiumRenderer(); 213 | await _sodium.ready 214 | const sodium = _sodium 215 | 216 | //console.log('ourKeyPair', ourKeyPair) 217 | const sEncode = ourKeyPair.privateKey.slice(0, 32) // only half 218 | 219 | const shaFullLength = sodium.crypto_hash_sha512(sEncode) 220 | 221 | const Hrh = shaFullLength.slice(32) 222 | 223 | const r = sodium.crypto_core_ed25519_scalar_reduce(await sha512Multipart([Hrh, kA, messageParts])) 224 | 225 | const sigR = sodium.crypto_scalarmult_ed25519_base_noclamp(r) 226 | 227 | const HRAM = sodium.crypto_core_ed25519_scalar_reduce(await sha512Multipart([sigR, kA, messageParts])) 228 | 229 | const sigS = sodium.crypto_core_ed25519_scalar_add( 230 | r, 231 | sodium.crypto_core_ed25519_scalar_mul(HRAM, ka) 232 | ) 233 | 234 | const fullSig = binary.concatUInt8Array(sigR, sigS) 235 | return fullSig 236 | } 237 | 238 | async function getSogsSignature(blinded, ka, kA, signingKeys, toSign) { 239 | await _sodium.ready 240 | const sodium = _sodium 241 | //console.log('blinded', blinded, 'ka', ka, 'kA', kA) 242 | if (blinded && ka && kA) { 243 | //console.log('signing sogs with blinded ED25519 sig') 244 | return blindedED25519Signature(toSign, signingKeys, ka, kA) 245 | } 246 | //console.log('signingKeys', signingKeys) 247 | //const edKeyPrivBytes = edKey.ed25519KeyPair.privateKey 248 | // signingKeys.privateKey is in Uint8Array 249 | //console.log('signing sogs with unblinded personal ED25519 sig') 250 | return sodium.crypto_sign_detached(toSign, signingKeys.privateKey) 251 | } 252 | 253 | // only need signingKeys.privateKey (ed priv key here) 254 | const getBlindingValues = async (serverPK, signingKeys) => { 255 | await _sodium.ready 256 | const sodium = _sodium 257 | const k = sodium.crypto_core_ed25519_scalar_reduce(sodium.crypto_generichash(64, serverPK)) 258 | //console.log('signingKeys', signingKeys) 259 | 260 | let signingKey = sodium.crypto_sign_ed25519_sk_to_curve25519(signingKeys.privateKey) 261 | 262 | if (signingKey.length > 32) { 263 | console.warn('length of signing key is too long, cutting to 32: oldlength', signingKey.length) 264 | signingKey = signingKey.slice(0, 32) 265 | } 266 | 267 | const ka = sodium.crypto_core_ed25519_scalar_mul(k, signingKey) // recast for reasons 268 | const kA = sodium.crypto_scalarmult_ed25519_base_noclamp(ka) 269 | 270 | return { 271 | a: signingKey, 272 | secretKey: ka, 273 | publicKey: kA, 274 | } 275 | } 276 | 277 | async function getSigDataBlinded(serverPubKeyHex, signingKeys, messageBuf) { 278 | const srvU8A = binary.hexStringToUint8Array(serverPubKeyHex) 279 | const blindKp = await getBlindingValues(srvU8A, signingKeys) 280 | // blindKp has a, secretKey, publicKey 281 | const ka = blindKp.secretKey 282 | const kA = blindKp.publicKey 283 | //console.log('signing with', ka, kA, 'and', signingKeys) 284 | 285 | const sigB64 = await getSogsSignature(true, ka, kA, signingKeys, messageBuf) 286 | //console.log('sig', sigBuf) 287 | 288 | if (0) { 289 | await _sodium.ready 290 | const sodium = _sodium 291 | console.log('mid', sigB64) 292 | const sigBuf = Buffer.from(sigB64, 'base64') 293 | 294 | const blindedVerifySig = sodium.crypto_sign_verify_detached( 295 | sigBuf, 296 | messageBuf, 297 | kA // this this right? 298 | ) 299 | console.log('blindedVerifySig', blindedVerifySig) 300 | } 301 | 302 | return binary.fromUInt8ArrayToBase64(sigB64) 303 | } 304 | 305 | async function verifySigDataV3(serverPubKeyHex, pubKeyBuf, messageBuf, sigB64) { 306 | //const srvU8A = binary.hexStringToUint8Array(serverPubKeyHex) 307 | const sigBuf = Buffer.from(sigB64, 'base64') 308 | //console.log('sigBufLength', sigBuf.byteLength) 309 | //console.log('blind type?', pubKeyBuf[0], 0x15) 310 | if (pubKeyBuf[0] === 0x15) { 311 | //console.log('verifying blinded message') 312 | // blinded 313 | await _sodium.ready 314 | const sodium = _sodium 315 | // kA 316 | const pubkeyWithoutPrefixBuf = pubKeyBuf.slice(1) // Buffer 317 | //console.log('pubkeyWithoutPrefix', typeof(pubkeyWithoutPrefix), pubkeyWithoutPrefix) 318 | 319 | const blindedVerifySig = sodium.crypto_sign_verify_detached( 320 | sigBuf, 321 | messageBuf, 322 | pubkeyWithoutPrefixBuf 323 | ) 324 | //console.log('blindedVerifySig', blindedVerifySig) 325 | 326 | return blindedVerifySig 327 | } 328 | // standard curve verify 329 | return libsignal.curve.verifySignature(pubKeyBuf, messageBuf, sigBuf) 330 | } 331 | 332 | // Calculate a shared secret for a message from A to B: 333 | // 334 | // BLAKE2b(a kB || kA || kB) 335 | // 336 | // The receiver can calulate the same value via: 337 | // 338 | // BLAKE2b(b kA || kA || kB) 339 | function sharedBlindedEncryptionKey(fromBlindedPublicKey, otherBlindedPublicKey, 340 | secretKey, sodium, toBlindedPublicKey) { 341 | // Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to 342 | // convert to an *x* secret key, which seems wrong--but isn't because converted keys use the 343 | // same secret scalar secret (and so this is just the most convenient way to get 'a' out of 344 | // a sodium Ed25519 secret key) 345 | //const aBytes = generatePrivateKeyScalar(secretKey, sodium) 346 | const aBytes = sodium.crypto_sign_ed25519_sk_to_curve25519(secretKey) 347 | const combinedKeyBytes = sodium.crypto_scalarmult_ed25519_noclamp(aBytes, otherBlindedPublicKey) 348 | return sodium.crypto_generichash(32, 349 | binary.concatUInt8Array(combinedKeyBytes, fromBlindedPublicKey, toBlindedPublicKey) 350 | ) 351 | } 352 | 353 | function generateBlindingFactor(serverPubKeyHex, sodium) { 354 | const serverUI8 = binary.hexStringToUint8Array(serverPubKeyHex) 355 | const serverPkHash = sodium.crypto_generichash(64, serverUI8) 356 | if (!serverPkHash.length) { 357 | throw new Error('generateBlindingFactor: crypto_generichash failed') 358 | } 359 | 360 | // Reduce the server public key into an ed25519 scalar (`k`) 361 | const k = sodium.crypto_core_ed25519_scalar_reduce(serverPkHash) 362 | return k 363 | } 364 | 365 | // https://github.com/oxen-io/session-desktop/blob/0794edeb69aac582187da35771dc29ae3e68279c/ts/session/crypto/BufferPadding.ts#L13 366 | /** 367 | * Unpad the buffer from its padding. 368 | * An error is thrown if there is no padding. 369 | * A padded buffer is 370 | * * whatever at start 371 | * * ends with 0x80 and any number of 0x00 until the end 372 | */ 373 | function removeMessagePadding(paddedData) { 374 | const paddedPlaintext = new Uint8Array(paddedData) 375 | // window?.log?.info('Removing message padding...'); 376 | for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) { 377 | if (paddedPlaintext[i] === 0x80) { 378 | const plaintext = new Uint8Array(i) 379 | plaintext.set(paddedPlaintext.subarray(0, i)) 380 | return plaintext.buffer 381 | } else if (paddedPlaintext[i] !== 0x00) { 382 | console.debug('got a message without padding... Letting it through for now') 383 | return paddedPlaintext 384 | } 385 | } 386 | 387 | throw new Error('Invalid padding') 388 | } 389 | 390 | async function decryptWithSessionBlindingProtocol( 391 | data, isOutgoing, otherBlindedPublicKey, serverPubKeyHex, userEd25519KeyPair) { 392 | await _sodium.ready 393 | const sodium = _sodium 394 | 395 | const NPUBBYTES = sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES 396 | if (data.length <= NPUBBYTES) { 397 | console.warn(`data is too short. should be at least ${sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES} but is ${data.length}`) 398 | return false 399 | } 400 | //console.log('serverPubKeyHex', serverPubKeyHex) 401 | //console.log('userEd25519KeyPair', userEd25519KeyPair) 402 | const srvU8A = binary.hexStringToUint8Array(serverPubKeyHex) 403 | const blindKp = await getBlindingValues(srvU8A, userEd25519KeyPair) 404 | if (!blindKp) { 405 | console.warn('decryptWithSessionBlindingProtocol - getBlindingValues failure') 406 | return false 407 | } 408 | const otherPkBuf = Buffer.from(otherBlindedPublicKey, 'hex') 409 | const otherPkWithoutPrefixBuf = otherPkBuf.slice(1) // Buffer 410 | const kA = isOutgoing ? blindKp.publicKey : otherPkWithoutPrefixBuf 411 | 412 | // probably needs a try 413 | const decKey = sharedBlindedEncryptionKey(kA, otherPkWithoutPrefixBuf, 414 | userEd25519KeyPair.privateKey, sodium, 415 | isOutgoing ? otherPkBuf : blindKp.publicKey, 416 | ) 417 | if (!decKey) { 418 | console.warn('decryptWithSessionBlindingProtocol - sharedBlindedEncryptionKey failure') 419 | return false 420 | } 421 | const version = data[0] 422 | const NPUBBYTESLoc = data.length - NPUBBYTES 423 | const ciphertext = data.slice(1, NPUBBYTESLoc) 424 | const nonce = data.slice(NPUBBYTESLoc) 425 | 426 | if (version !== 0) { 427 | console.warn('decryptWithSessionBlindingProtocol - Unknown version', version) 428 | return false 429 | } 430 | // We can decrypt this! We have the technology! 431 | const innerBytes = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( 432 | null, 433 | ciphertext, 434 | null, 435 | nonce, 436 | decKey 437 | ) 438 | if (!innerBytes) { 439 | console.warn('decryptWithSessionBlindingProtocol - decryption failed') 440 | return false 441 | } 442 | const numBytesPubkey = 32 443 | // Ensure the length is correct 444 | if (innerBytes.length <= numBytesPubkey) { 445 | console.warn('decryptWithSessionBlindingProtocol - decryption failed, result too small') 446 | return false 447 | } 448 | 449 | // Split up: the last 32 bytes are the sender's *unblinded* ed25519 key 450 | const senderEdpkLoc = innerBytes.length - numBytesPubkey 451 | const plainText = innerBytes.slice(0, senderEdpkLoc) 452 | const senderEdpk = innerBytes.slice(senderEdpkLoc) 453 | 454 | // Verify that the inner sender_edpk (A) yields the same outer kA we got with the message 455 | const blindingFactor = generateBlindingFactor(serverPubKeyHex, sodium) 456 | //const sharedSecret = combineKeys(blindingFactor, senderEdpk, sodium); 457 | const sharedSecret = sodium.crypto_scalarmult_ed25519_noclamp(blindingFactor, senderEdpk) 458 | 459 | // case insensitive compare 460 | // kA is a buffer 461 | // sharedSecret is uint8 462 | const sharedSecretBuf = Buffer.from(sharedSecret.buffer, sharedSecret.byteOffset, sharedSecret.byteLength) 463 | //console.log('decryptWithSessionBlindingProtocol - kA', kA, '==', sharedSecretBuf) 464 | if (Buffer.compare(kA, sharedSecretBuf) !== 0) { 465 | console.warn('decryptWithSessionBlindingProtocol - kA', kA, '!=', sharedSecret) 466 | return false 467 | } 468 | 469 | // Get the sender's X25519 public key 470 | //const senderSessionIdBytes = toX25519(senderEdpk, sodium) 471 | const senderSessionIdBytes = sodium.crypto_sign_ed25519_pk_to_curve25519(senderEdpk) 472 | // Uint8Array 473 | //console.log('senderSessionIdBytes', senderSessionIdBytes) 474 | const senderPKBuf = Buffer.from(senderSessionIdBytes.buffer, senderSessionIdBytes.byteOffset, senderSessionIdBytes.byteLength) 475 | const plainTextBuf = Buffer.from(plainText.buffer, plainText.byteOffset, plainText.byteLength) 476 | return { 477 | plainTextBuf, 478 | senderUnblinded: '05' + senderPKBuf.toString('hex') 479 | //senderUnblinded: `${KeyPrefixType.standard}${to_hex(senderSessionIdBytes)}` 480 | } 481 | } 482 | 483 | module.exports = { 484 | DHEncrypt, 485 | DHDecrypt, 486 | DHEncrypt64, 487 | DHDecrypt64, 488 | generateEphemeralKeyPair, 489 | // what needs this? 490 | makeSymmetricKey, 491 | makeOnionSymKey, 492 | encryptGCM, 493 | decryptGCM, 494 | encryptCBC, 495 | decryptCBC, 496 | getSigData, 497 | getSigDataV2, 498 | getSigDataBlinded, 499 | verifySigDataV2, 500 | getBlindingValues, 501 | getSogsSignature, 502 | verifySigDataV3, 503 | decryptWithSessionBlindingProtocol, 504 | removeMessagePadding, 505 | } 506 | --------------------------------------------------------------------------------