├── .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 | 
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 |
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 |
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 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | - License:
92 |
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 | | Name |
184 |
185 |
186 | Type |
187 |
188 |
189 |
190 |
191 |
192 | Description |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 | lnsName |
202 |
203 |
204 |
205 |
206 |
207 | String
208 |
209 |
210 |
211 | |
212 |
213 |
214 |
215 |
216 |
217 | what name to look up |
218 |
219 |
220 |
221 |
222 |
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 | | Name |
343 |
344 |
345 | Type |
346 |
347 |
348 |
349 |
350 |
351 | Description |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 | lnsName |
361 |
362 |
363 |
364 |
365 |
366 | String
367 |
368 |
369 |
370 | |
371 |
372 |
373 |
374 |
375 |
376 | what name to look up |
377 |
378 |
379 |
380 |
381 |
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 |
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",/^[^]+/],["dec",/^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^
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 | | Name |
70 |
71 |
72 | Type |
73 |
74 |
75 |
76 |
77 |
78 | Description |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | pollRate |
88 |
89 |
90 |
91 |
92 |
93 | Number
94 |
95 |
96 |
97 | |
98 |
99 |
100 |
101 |
102 |
103 | How much delay between poll requests |
104 |
105 |
106 |
107 |
108 |
109 |
110 | lastHash |
111 |
112 |
113 |
114 |
115 |
116 | Number
117 |
118 |
119 |
120 | |
121 |
122 |
123 |
124 |
125 |
126 | Poll for messages from this hash on |
127 |
128 |
129 |
130 |
131 |
132 |
133 | displayName |
134 |
135 |
136 |
137 |
138 |
139 | String
140 |
141 |
142 |
143 | |
144 |
145 |
146 |
147 |
148 |
149 | Send messages with this profile name |
150 |
151 |
152 |
153 |
154 |
155 |
156 | homeServer |
157 |
158 |
159 |
160 |
161 |
162 | String
163 |
164 |
165 |
166 | |
167 |
168 |
169 |
170 |
171 |
172 | URL for this identity's file server |
173 |
174 |
175 |
176 |
177 |
178 |
179 | homeServerPubKey |
180 |
181 |
182 |
183 |
184 |
185 | String
186 |
187 |
188 |
189 | |
190 |
191 |
192 |
193 |
194 |
195 | Pubkey in hex for this identity's file server |
196 |
197 |
198 |
199 |
200 |
201 |
202 | identityOutput |
203 |
204 |
205 |
206 |
207 |
208 | String
209 |
210 |
211 |
212 | |
213 |
214 |
215 |
216 |
217 |
218 | human readable string with seed words if generated a new identity |
219 |
220 |
221 |
222 |
223 |
224 |
225 | ourPubkeyHex |
226 |
227 |
228 |
229 |
230 |
231 | String
232 |
233 |
234 |
235 | |
236 |
237 |
238 |
239 |
240 |
241 | This identity's pubkey (SessionID) |
242 |
243 |
244 |
245 |
246 |
247 |
248 | keypair |
249 |
250 |
251 |
252 |
253 |
254 | object
255 |
256 |
257 |
258 | |
259 |
260 |
261 |
262 |
263 |
264 | This identity's keypair buffers |
265 |
266 |
267 |
268 |
269 |
270 |
271 | open |
272 |
273 |
274 |
275 |
276 |
277 | Boolean
278 |
279 |
280 |
281 | |
282 |
283 |
284 |
285 |
286 |
287 | Should we continue polling for messages |
288 |
289 |
290 |
291 |
292 |
293 |
294 | encAvatarUrl |
295 |
296 |
297 |
298 |
299 |
300 | String
301 |
302 |
303 |
304 | |
305 |
306 |
307 |
308 |
309 |
310 | Encrypted avatar URL |
311 |
312 |
313 |
314 |
315 |
316 |
317 | profileKeyBuf |
318 |
319 |
320 |
321 |
322 |
323 | Buffer
324 |
325 |
326 |
327 | |
328 |
329 |
330 |
331 |
332 |
333 | Key to decrypt avatar URL |
334 |
335 |
336 |
337 |
338 |
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 |
374 |
375 |
376 |
377 |
378 |
379 |
380 | - License:
381 |
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 | | Name |
489 |
490 |
491 | Type |
492 |
493 |
494 |
495 |
496 |
497 | Description |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 | messages |
507 |
508 |
509 |
510 |
511 |
512 | Array
513 |
514 |
515 |
516 | |
517 |
518 |
519 |
520 |
521 |
522 | an array of Content protobuf |
523 |
524 |
525 |
526 |
527 |
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 | | Name |
626 |
627 |
628 | Type |
629 |
630 |
631 |
632 |
633 |
634 | Description |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 | hash |
644 |
645 |
646 |
647 |
648 |
649 | String
650 |
651 |
652 |
653 | |
654 |
655 |
656 |
657 |
658 |
659 | The last hash returns from the storage server for this pubkey |
660 |
661 |
662 |
663 |
664 |
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 |
752 |
753 |
754 |
755 |
756 |