Merge branch '5.4-rc'
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
.gitmodules
|
||||
.github
|
||||
docker-compose.yml
|
||||
traefik2.yml
|
||||
Dockerfile*
|
||||
*.png
|
||||
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
@ -1,5 +1,5 @@
|
||||
[ignore]
|
||||
.*/bower_components/.*
|
||||
.*/components/.*
|
||||
.*/node_modules/lesshint/*
|
||||
[include]
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_resolution.yml
vendored
@ -85,7 +85,7 @@ body:
|
||||
label: Version
|
||||
description: What version of CryptPad are you running?
|
||||
options:
|
||||
- 5.4-rc
|
||||
- 5.4.0
|
||||
- 5.3.0
|
||||
- 5.2.1
|
||||
- 5.2.0
|
||||
|
||||
2
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
datastore
|
||||
tasks
|
||||
www/components/*
|
||||
www/bower_components/*
|
||||
www/accounts
|
||||
node_modules
|
||||
@ -22,4 +23,3 @@ block/
|
||||
logs/
|
||||
privileged.conf
|
||||
config/config.js
|
||||
*.sh
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
www/components/
|
||||
www/bower_components/
|
||||
www/common/onlyoffice/sdkjs
|
||||
www/common/onlyoffice/web-apps
|
||||
|
||||
72
CHANGELOG.md
@ -1,3 +1,75 @@
|
||||
# 5.4.0
|
||||
|
||||
## Goals
|
||||
|
||||
This release introduces two major new features:
|
||||
- New Diagram application
|
||||
- 2 factor authentication using time-based one-time passwords (TOTP)
|
||||
|
||||
Also included are some improvements, dependency updates, and bug fixes
|
||||
|
||||
## Features
|
||||
|
||||
- Diagram application: integration of [Draw.io](https://www.drawio.com/) with CryptPad's encrypted real time collaboration [[#1070](https://github.com/cryptpad/cryptpad/pull/1070)]
|
||||
- Introduce a new app color for Diagram and adjust Whiteboard color [[#1059](https://github.com/cryptpad/cryptpad/issues/1059)]
|
||||
- New 2 Factor Authentication with TOTP [[#1071](https://github.com/cryptpad/cryptpad/pull/1071)]. To enable for a user account:
|
||||
1. Settings > Security & Privacy
|
||||
2. Enter your password
|
||||
3. Save the recovery code
|
||||
4. Snap the QR code with a 2FA app of your choice
|
||||
5. ✅ 2FA is enabled
|
||||
- Docker deployment is now officially supported [[#1064](https://github.com/cryptpad/cryptpad/pull/1064)]
|
||||
|
||||
## Improvements
|
||||
|
||||
- New setting to destroy all documents of which you are the sole owner
|
||||
- Settings re-organization
|
||||
- Add favicons in ICO format [[#1068](https://github.com/cryptpad/cryptpad/pull/1068) thanks @lemondevxyz]
|
||||
|
||||
## Bugs / issues
|
||||
|
||||
- Form
|
||||
- Make Form question text selectable in participant view [[#1046](https://github.com/cryptpad/cryptpad/issues/1046)]
|
||||
- Add form title to archived notifications [[#1065](https://github.com/cryptpad/cryptpad/pull/1065) thanks to @lemondevxyz]
|
||||
- Add "make a copy" to office editors [[#1067](https://github.com/cryptpad/cryptpad/pull/1067) thanks to @lemondevxyz]
|
||||
- Disable the "protect tab" feature in Sheets as it cannot be integrated in CryptPad [[#1053](https://github.com/cryptpad/cryptpad/issues/1053)]
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Remove Bower to manage client side dependencies [[#989](https://github.com/cryptpad/cryptpad/pull/989) [#1072](https://github.com/cryptpad/cryptpad/pull/1072) thanks to @Pamplemousse] ⚠️ Please read upgrade notes carefully if you administer an instance
|
||||
- Upgrade Mermaid diagrams to 10.2.4 [[#1118](https://github.com/cryptpad/cryptpad/issues/1118)]
|
||||
- Upgrade CKeditor to 4.22.1 [[#1119](https://github.com/cryptpad/cryptpad/issues/1119)]
|
||||
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
⚠️ Please read upgrade notes carefully as this version introduces breaking changes
|
||||
|
||||
If you are upgrading from a version older than `5.3.0` please read the upgrade notes of all versions between yours and `5.4.0` to avoid configuration issues.
|
||||
|
||||
To upgrade:
|
||||
|
||||
1. Stop your server
|
||||
2. Get the latest code with git
|
||||
```bash
|
||||
git fetch origin --tags
|
||||
git checkout 5.4.0
|
||||
```
|
||||
3. Major changes to the Nginx config
|
||||
- Access-Control-Allow-Credentials header
|
||||
- proxy_pass request for /blob/ and /block/ to the node process
|
||||
- new port for the websocket
|
||||
- set CSP headers for draw.io, used by the new diagram app
|
||||
- see the [full diff](https://github.com/cryptpad/cryptpad/compare/5.4-rc#diff-a97d166145edec9545df5228d500c144bd5ec20db759cf5cc6f90309e963b1ca)
|
||||
4. Bower removed
|
||||
- To download all dependencies, use `npm install`
|
||||
- Then, to copy client-side dependencies, use `npm run install:components`
|
||||
- `www/bower_components` can be removed
|
||||
5. If you have previously used the `build` command to enable opengraph preview images
|
||||
- Please run `npm run build` again after upgrading
|
||||
6. Restart your server
|
||||
7. Review your instance's checkup page to ensure that you are passing all tests
|
||||
|
||||
# 5.3.0
|
||||
|
||||
## Goals
|
||||
|
||||
51
Dockerfile
Normal file
@ -0,0 +1,51 @@
|
||||
# Multistage build to reduce image size and increase security
|
||||
FROM node:lts-slim AS build
|
||||
|
||||
# Create folder for CryptPad
|
||||
RUN mkdir /cryptpad
|
||||
WORKDIR /cryptpad
|
||||
|
||||
# Copy CryptPad source code to the container
|
||||
COPY . /cryptpad
|
||||
|
||||
RUN sed -i "s@//httpAddress: '::'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
|
||||
RUN sed -i "s@installMethod: 'unspecified'@installMethod: 'docker'@" /cryptpad/config/config.example.js
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --production \
|
||||
&& npm run install:components
|
||||
|
||||
# Create actual CryptPad image
|
||||
FROM node:lts-slim
|
||||
|
||||
# Create user and group for CryptPad so it does not run as root
|
||||
RUN groupadd cryptpad -g 4001
|
||||
RUN useradd cryptpad -u 4001 -g 4001 -d /cryptpad
|
||||
|
||||
# Copy cryptpad with installed modules
|
||||
COPY --from=build --chown=cryptpad /cryptpad /cryptpad
|
||||
USER cryptpad
|
||||
|
||||
# Copy docker-entrypoint.sh script
|
||||
COPY --chown=cryptpad docker-entrypoint.sh /cryptpad/docker-entrypoint.sh
|
||||
|
||||
# Set workdir to cryptpad
|
||||
WORKDIR /cryptpad
|
||||
|
||||
# Create directories
|
||||
RUN mkdir blob block customize data datastore
|
||||
|
||||
# Volumes for data persistence
|
||||
VOLUME /cryptpad/blob
|
||||
VOLUME /cryptpad/block
|
||||
VOLUME /cryptpad/customize
|
||||
VOLUME /cryptpad/data
|
||||
VOLUME /cryptpad/datastore
|
||||
|
||||
ENTRYPOINT ["/bin/bash", "/cryptpad/docker-entrypoint.sh"]
|
||||
|
||||
# Ports
|
||||
EXPOSE 3000 3001
|
||||
|
||||
# Run cryptpad on startup
|
||||
CMD ["npm", "start"]
|
||||
57
bower.json
@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "cryptpad",
|
||||
"version": "0.1.0",
|
||||
"authors": [
|
||||
"Caleb James DeLisle <cjd@cjdns.fr>"
|
||||
],
|
||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
||||
"main": "www/index.html",
|
||||
"moduleType": [
|
||||
"node"
|
||||
],
|
||||
"license": "AGPLv3",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": "3.6.0",
|
||||
"tweetnacl": "0.12.2",
|
||||
"components-font-awesome": "^4.6.3",
|
||||
"ckeditor": "4.14.0",
|
||||
"codemirror": "^5.19.0",
|
||||
"requirejs": "2.3.5",
|
||||
"marked": "1.1.0",
|
||||
"rangy": "rangy-release#~1.3.0",
|
||||
"json.sortify": "~2.1.0",
|
||||
"hyperjson": "~1.4.0",
|
||||
"chainpad-crypto": "^0.2.0",
|
||||
"chainpad-listmap": "^1.0.0",
|
||||
"chainpad": "^5.2.0",
|
||||
"file-saver": "1.3.1",
|
||||
"alertifyjs": "1.0.11",
|
||||
"scrypt-async": "1.2.0",
|
||||
"require-css": "0.1.10",
|
||||
"bootstrap": "^v4.0.0",
|
||||
"diff-dom": "2.1.1",
|
||||
"nthen": "0.1.7",
|
||||
"open-sans-fontface": "^1.4.2",
|
||||
"bootstrap-tokenfield": "0.12.1",
|
||||
"localforage": "^1.5.2",
|
||||
"html2canvas": "^0.4.1",
|
||||
"croppie": "^2.5.0",
|
||||
"sortablejs": "^1.6.0",
|
||||
"saferphore": "^0.0.1",
|
||||
"jszip": "3.7.1",
|
||||
"requirejs-plugins": "^1.0.3",
|
||||
"dragula.js": "3.7.2",
|
||||
"MathJax": "3.0.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"bootstrap": "^v4.0.0",
|
||||
"jquery": "3.6.0"
|
||||
}
|
||||
}
|
||||
@ -92,6 +92,19 @@ module.exports = {
|
||||
*/
|
||||
//httpSafePort: 3001,
|
||||
|
||||
/* Websockets need to be exposed on a separate port from the rest of
|
||||
* the platform's HTTP traffic. Port 3003 is used by default.
|
||||
* You can change this to a different port if it is in use by a
|
||||
* different service, but under most circumstances you can leave this
|
||||
* commented and it will work.
|
||||
*
|
||||
* In production environments, your reverse proxy (usually NGINX)
|
||||
* will need to forward websocket traffic (/cryptpad_websocket)
|
||||
* to this port.
|
||||
*
|
||||
*/
|
||||
// websocketPort: 3003,
|
||||
|
||||
/* CryptPad will launch a child process for every core available
|
||||
* in order to perform CPU-intensive tasks in parallel.
|
||||
* Some host environments may have a very large number of cores available
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
|
||||
@ -62,7 +62,7 @@ CKEDITOR.editorConfig = function( config ) {
|
||||
// every part of ckeditor will get in the browser cache.
|
||||
var fix = function (x) {
|
||||
if (x.map) { return x.map(fix); }
|
||||
return (/\/bower_components\/.*\.css$/.test(x)) ? (x + '?ver=' + CKEDITOR.timestamp) : x;
|
||||
return (/\/components\/.*\.css$/.test(x)) ? (x + '?ver=' + CKEDITOR.timestamp) : x;
|
||||
};
|
||||
CKEDITOR.tools._buildStyleHtml = CKEDITOR.tools.buildStyleHtml;
|
||||
CKEDITOR.document._appendStyleSheet = CKEDITOR.document.appendStyleSheet;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript></noscript>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
define([
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
], function (ChainPad) {
|
||||
var Diff = ChainPad.Diff;
|
||||
|
||||
|
||||
BIN
customize.dist/favicon.ico
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
customize.dist/favicon/alt-favicon-code.ico
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
customize.dist/favicon/alt-favicon-diagram.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/alt-favicon-diagram.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
customize.dist/favicon/alt-favicon-doc.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-form.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-kanban.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-pad.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-poll.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-presentation.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-sheet.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/alt-favicon-slide.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/alt-favicon-whiteboard.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
BIN
customize.dist/favicon/alt-favicon.ico
Normal file
|
After Width: | Height: | Size: 169 KiB |
@ -6,8 +6,8 @@
|
||||
viewBox="0 0 135.46606 135.46728"
|
||||
id="svg942"
|
||||
sodipodi:docname="favicon_source.svg"
|
||||
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
|
||||
inkscape:export-filename="/home/david/cryptpad/customize.dist/favicon/alt-favicon-document.png"
|
||||
inkscape:version="1.2.2 (1:1.2.2+202305151914+b0a8486541)"
|
||||
inkscape:export-filename="alt-favicon-diagram.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
@ -32,6 +32,14 @@
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs946">
|
||||
<linearGradient
|
||||
id="linearGradient1098"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#ce3ad3;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop1096" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient943"
|
||||
inkscape:swatch="solid">
|
||||
@ -81,6 +89,15 @@
|
||||
x2="458.45312"
|
||||
y2="256.63477"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1098"
|
||||
id="linearGradient1100"
|
||||
x1="53.544922"
|
||||
y1="256.63476"
|
||||
x2="458.45312"
|
||||
y2="256.63476"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
@ -92,18 +109,20 @@
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1832"
|
||||
inkscape:window-height="1133"
|
||||
inkscape:window-height="1128"
|
||||
id="namedview944"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.819555"
|
||||
inkscape:cx="60.398631"
|
||||
inkscape:cy="380.6944"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:cx="12.811831"
|
||||
inkscape:cy="384.96501"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="745"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="g952"
|
||||
inkscape:current-layer="g1160"
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:pagecheckerboard="0" />
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
@ -270,7 +289,7 @@
|
||||
inkscape:groupmode="layer"
|
||||
id="g373"
|
||||
inkscape:label="[export] shield [OO_doc]"
|
||||
style="display:inline">
|
||||
style="display:none">
|
||||
<g
|
||||
id="g371"
|
||||
transform="matrix(1.4853714,0,0,1.4853714,12.798765,-0.61151946)">
|
||||
@ -593,24 +612,62 @@
|
||||
style="fill:#2c9e98;fill-opacity:1">
|
||||
<path
|
||||
d="m 128.98,30.355 0.55499,39.644 h 33.141 l 0.004,-39.644 z"
|
||||
style="fill:#a72ba7;fill-opacity:0.4"
|
||||
style="fill:#8f40f5;fill-opacity:0.4"
|
||||
id="path1009" />
|
||||
<path
|
||||
d="m 162.69,70 0.003,43.946 c 12.825,-5.8796 32.762,-17.077 33.127,-43.157 l 0.0108,-0.78911 z"
|
||||
style="fill:#a72ba7;fill-opacity:0.4"
|
||||
style="fill:#8f40f5;fill-opacity:0.4"
|
||||
id="path1011" />
|
||||
<path
|
||||
id="path1013"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#a72ba7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:29.7103;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:url(#linearGradient1100);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:29.7103;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 53.544922,4.7617188 0.185547,15.0449222 3.179687,256.695309 c 0.953422,76.9836 32.1134,127.84094 71.929684,161.46875 39.81629,33.62784 87.45761,51.28822 122.63086,67.4668 l 6.66602,3.07031 6.48047,-3.43359 c 31.31498,-16.53917 79.28982,-34.11259 120.05273,-67.35547 40.76297,-33.24293 73.7832,-83.90225 73.7832,-161.40234 V 130.05664 L 323.60742,4.7617188 Z M 83.621094,34.476562 H 291.42578 V 160.75781 H 428.75 v 115.5586 c 0,69.39522 -27.09036,109.2031 -62.86133,138.375 -33.66112,27.45122 -74.94064,43.84038 -108.41015,60.82226 C 221.97696,459.48582 180.81444,442.98268 148.00586,415.27344 113.30228,385.96366 87.475071,345.55977 86.615234,276.13086 Z m 237.519526,8.556641 94.7461,88.021487 h -94.7461 z"
|
||||
transform="matrix(0.17812685,0,0,0.17812685,116.76305,26.860695)" />
|
||||
<g
|
||||
transform="matrix(1.1107,0,0,1.1107,18.926,21.932)"
|
||||
style="fill:#a72ba7;fill-opacity:1"
|
||||
style="fill:#8f40f5;fill-opacity:1"
|
||||
id="g1017">
|
||||
<path
|
||||
id="path1015"
|
||||
style="fill:#a72ba7;fill-opacity:1;stroke-width:6.23544"
|
||||
style="fill:#8f40f5;fill-opacity:1;stroke-width:6.23544"
|
||||
d="m 255.8125,188.08008 a 50.622452,50.622452 0 0 0 -50.62305,50.62304 50.622452,50.622452 0 0 0 28.73243,45.64258 L 216.25,377.66602 h 79.14648 l -17.67382,-93.33008 a 50.622452,50.622452 0 0 0 28.71289,-45.63282 50.622452,50.622452 0 0 0 -50.62305,-50.62304 z"
|
||||
transform="matrix(0.1603735,0,0,0.1603735,88.085934,4.437467)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1160"
|
||||
inkscape:label="[export] shield [diagram]"
|
||||
style="display:inline">
|
||||
<g
|
||||
id="g1158"
|
||||
transform="matrix(1.4853714,0,0,1.4853714,12.798765,-0.61151946)">
|
||||
<g
|
||||
transform="translate(-125.38,-26.449)"
|
||||
id="g1156"
|
||||
style="fill:#2c9e98;fill-opacity:1">
|
||||
<path
|
||||
d="m 128.98,30.355 0.55499,39.644 h 33.141 l 0.004,-39.644 z"
|
||||
style="fill:#ce3ad3;fill-opacity:0.40000001"
|
||||
id="path1146" />
|
||||
<path
|
||||
d="m 162.69,70 0.003,43.946 c 12.825,-5.8796 32.762,-17.077 33.127,-43.157 l 0.0108,-0.78911 z"
|
||||
style="fill:#ce3ad3;fill-opacity:0.40000001"
|
||||
id="path1148" />
|
||||
<path
|
||||
id="path1150"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:url(#linearGradient1100);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:29.7103;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 53.544922,4.7617188 0.185547,15.0449222 3.179687,256.695309 c 0.953422,76.9836 32.1134,127.84094 71.929684,161.46875 39.81629,33.62784 87.45761,51.28822 122.63086,67.4668 l 6.66602,3.07031 6.48047,-3.43359 c 31.31498,-16.53917 79.28982,-34.11259 120.05273,-67.35547 40.76297,-33.24293 73.7832,-83.90225 73.7832,-161.40234 V 130.05664 L 323.60742,4.7617188 Z M 83.621094,34.476562 H 291.42578 V 160.75781 H 428.75 v 115.5586 c 0,69.39522 -27.09036,109.2031 -62.86133,138.375 -33.66112,27.45122 -74.94064,43.84038 -108.41015,60.82226 C 221.97696,459.48582 180.81444,442.98268 148.00586,415.27344 113.30228,385.96366 87.475071,345.55977 86.615234,276.13086 Z m 237.519526,8.556641 94.7461,88.021487 h -94.7461 z"
|
||||
transform="matrix(0.17812685,0,0,0.17812685,116.76305,26.860695)" />
|
||||
<g
|
||||
transform="matrix(1.1107,0,0,1.1107,18.926,21.932)"
|
||||
style="fill:#8f40f5;fill-opacity:1"
|
||||
id="g1154">
|
||||
<path
|
||||
id="path1152"
|
||||
style="fill:#ce3ad3;fill-opacity:1;stroke-width:6.23544"
|
||||
d="m 255.8125,188.08008 a 50.622452,50.622452 0 0 0 -50.62305,50.62304 50.622452,50.622452 0 0 0 28.73243,45.64258 L 216.25,377.66602 h 79.14648 l -17.67382,-93.33008 a 50.622452,50.622452 0 0 0 28.71289,-45.63282 50.622452,50.622452 0 0 0 -50.62305,-50.62304 z"
|
||||
transform="matrix(0.1603735,0,0,0.1603735,88.085934,4.437467)" />
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 44 KiB |
BIN
customize.dist/favicon/main-favicon-code.ico
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
customize.dist/favicon/main-favicon-diagram.ico
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
customize.dist/favicon/main-favicon-diagram.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
customize.dist/favicon/main-favicon-doc.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-form.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-kanban.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-pad.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-poll.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-presentation.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-sheet.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
customize.dist/favicon/main-favicon-slide.ico
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
customize.dist/favicon/main-favicon-teams.ico
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
customize.dist/favicon/main-favicon-whiteboard.ico
Normal file
|
After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
BIN
customize.dist/favicon/main-favicon.ico
Normal file
|
After Width: | Height: | Size: 168 KiB |
@ -8,7 +8,7 @@
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript></noscript>
|
||||
|
||||
@ -40,4 +40,5 @@
|
||||
<glyph unicode="" glyph-name="form-poll-maybe" horiz-adv-x="1094" d="M856.492 569.333c0 9.987-3.996 19.973-11.187 27.163l-54.325 54.327c-7.191 7.191-17.175 11.185-27.163 11.185s-19.972-3.995-27.163-11.185l-262.048-262.447-117.442 117.841c-7.191 7.19-17.175 11.185-27.163 11.185s-19.972-3.995-27.163-11.185l-54.327-54.327c-7.191-7.191-11.187-17.177-11.187-27.163s3.995-19.973 11.187-27.163l198.932-198.932c7.191-7.191 17.175-11.185 27.163-11.185s19.972 3.995 27.163 11.185l343.538 343.537c7.192 7.191 11.187 17.177 11.187 27.163zM65.903 444.364q0 120.454 35 225.454 35.454 105 101.818 184.090h73.636q-65.454-87.727-98.636-192.727-32.727-105-32.727-215.908 0-109.091 33.636-213.181t96.818-189.999h-72.727q-66.818 77.273-101.818 180.454t-35 221.818zM1027.916 444.364q0-119.545-35.454-222.727-35-103.182-101.363-179.545h-72.727q63.182 85.454 96.818 189.545 33.636 104.545 33.636 213.636 0 110.909-33.182 215.908-32.727 105-98.182 192.727h73.636q66.818-79.545 101.818-184.999 35-105 35-224.545z" />
|
||||
<glyph unicode="" glyph-name="destroy" horiz-adv-x="1094" d="M191.671 946.511c-28.024 0-50.742-22.731-50.742-50.756v-399.384h-74.822c-29.561-0.003-53.524-23.966-53.527-53.527 0.025-29.545 23.982-53.484 53.527-53.487h174.773l-88.853-95.929 95.929-104.66-95.835-104.524 121.3-132.182 43.855 55.423-70.369 76.813 95.781 104.471-95.687 104.364 89.122 96.225h190.445l-88.853-95.929 95.929-104.66-95.781-104.524 121.246-132.182 43.855 55.423-70.369 76.813 95.781 104.471-95.687 104.364 89.122 96.225h190.445l-88.853-95.929 95.983-104.66-95.835-104.524 121.286-132.182 43.815 55.423-70.383 76.759 95.835 104.524-95.727 104.417 89.122 96.171h117.318c29.55-0.004 53.515 23.936 53.54 53.487-0.003 29.566-23.974 53.531-53.54 53.527h-71.728v162.501c0 28.024-16.394 67.15-35.958 86.714l-164.966 164.966c-19.564 19.564-58.69 35.958-86.714 35.958zM208.607 878.832h406.073v-219.96c0-28.024 22.745-50.769 50.769-50.769h219.96v-111.732h-676.802zM682.359 874.608c8.989-3.172 17.984-7.936 21.685-11.636l165.505-165.505c3.702-3.7 8.463-12.696 11.636-21.685h-198.826z" />
|
||||
<glyph unicode="" glyph-name="drive" horiz-adv-x="878" d="M586.884 124.372c0 23.946-19.592 43.537-43.537 43.537s-43.537-19.592-43.537-43.537c0-23.946 19.592-43.537 43.537-43.537s43.537 19.592 43.537 43.537zM726.203 124.372c0 23.946-19.592 43.537-43.537 43.537s-43.537-19.592-43.537-43.537c0-23.946 19.592-43.537 43.537-43.537s43.537 19.592 43.537 43.537zM787.155 37.298c0-9.252-8.163-17.415-17.415-17.415h-661.767c-9.252 0-17.415 8.163-17.415 17.415v174.149c0 9.252 8.163 17.415 17.415 17.415h661.767c9.252 0 17.415-8.163 17.415-17.415zM117.77 298.521l85.442 262.312c2.721 9.252 13.061 16.326 22.857 16.326h425.577c9.796 0 20.136-7.075 22.857-16.326l85.442-262.312zM856.815 211.447c0 14.694-4.354 27.211-8.707 40.816l-107.211 329.795c-12.517 38.095-48.979 64.762-89.251 64.762h-425.577c-40.272 0-76.734-26.667-89.251-64.762l-107.211-329.795c-4.354-13.605-8.707-26.122-8.707-40.816v-174.149c0-47.891 39.184-87.075 87.075-87.075h661.767c47.891 0 87.075 39.184 87.075 87.075z" />
|
||||
<glyph unicode="" glyph-name="diagram" horiz-adv-x="878" d="M827.746 734.667l-173.333 173.333c-20.556 20.556-61.667 37.778-91.111 37.778h-497.778c-29.444 0-53.333-23.889-53.333-53.333v-888.889c0-29.444 23.889-53.333 53.333-53.333h746.667c29.444 0 53.333 23.889 53.333 53.333v640c0 29.444-17.222 70.556-37.778 91.111zM581.079 870.222c9.444-3.333 18.889-8.333 22.778-12.222l173.889-173.889c3.889-3.889 8.889-13.333 12.222-22.778h-208.889zM794.413 21.333h-711.111v853.333h426.667v-231.111c0-29.444 23.889-53.333 53.333-53.333h231.111zM142.19 699.381v-233.667h242.476l103.19-129.667-66.571 7.571c-0.464 0.060-1 0.095-1.544 0.095-6.469 0-11.802-4.862-12.545-11.13l-0.006-0.060c-0.043-0.393-0.068-0.848-0.068-1.309 0-6.459 4.847-11.786 11.103-12.542l0.060-0.006 119.095-13.714 13.667 119.095c0.059 0.459 0.093 0.99 0.093 1.529 0 6.519-4.937 11.884-11.276 12.562l-0.055 0.005c-0.401 0.045-0.865 0.071-1.336 0.071-6.528 0-11.899-4.95-12.564-11.302l-0.005-0.055-7.571-66.571-101.952 128.143v210.952h-274.19zM190.952 650.619h176.667v-136.143h-176.667v136.143zM628.143 307.952c-64.030 0-117.525-47.559-126.857-109.095l-73.095 0.762 47.857 46.81c2.398 2.303 3.888 5.536 3.888 9.117 0 3.487-1.413 6.645-3.698 8.931v0c-2.288 2.295-5.452 3.714-8.947 3.714-0.018 0-0.037 0-0.055 0h0.003c-0.058 0.001-0.127 0.002-0.196 0.002-3.425 0-6.532-1.363-8.807-3.576l0.003 0.003-85.524-83.81 83.809-85.714c2.293-2.327 5.478-3.768 9.001-3.768 3.427 0 6.536 1.365 8.812 3.58l-0.003-0.003c2.327 2.293 3.768 5.478 3.768 9.001 0 3.427-1.365 6.536-3.58 8.812l0.003-0.003-46.952 47.857 73.81-0.714c9.614-61.186 62.962-108.286 126.762-108.286 70.545 0 128.095 57.551 128.095 128.095s-57.551 128.286-128.095 128.286zM626.095 259.19c0.686 0.017 1.357 0 2.048 0 44.192 0 79.333-35.332 79.333-79.524s-35.141-79.524-79.333-79.524c-44.192 0-79.524 35.332-79.524 79.524 0 43.501 34.254 78.443 77.476 79.524zM244.048 438.143l-85.381-84.048c-2.384-2.301-3.864-5.525-3.864-9.094 0-3.475 1.403-6.623 3.674-8.907l-0.001 0.001c2.295-2.341 5.49-3.792 9.024-3.792s6.729 1.451 9.022 3.79l0.002 0.002 47.571 46.762-0.952-117.667h-80.952v-191.286h202.381v191.286h-82.381l0.905 117.571 47.143-47.81c2.29-2.312 5.466-3.744 8.977-3.744 3.439 0 6.557 1.374 8.835 3.603l-0.002-0.002c2.384 2.301 3.864 5.525 3.864 9.094 0 3.475-1.403 6.623-3.674 8.907l0.001-0.001-84.19 85.333zM190.952 216.429h104.857v-93.762h-104.857v93.762z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 44 KiB |
@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'cptools';
|
||||
src: url('fonts/cptools.eot?pmxg4b');
|
||||
src: url('fonts/cptools.eot?pmxg4b#iefix') format('embedded-opentype'),
|
||||
url('fonts/cptools.ttf?pmxg4b') format('truetype'),
|
||||
url('fonts/cptools.woff?pmxg4b') format('woff'),
|
||||
url('fonts/cptools.svg?pmxg4b#cptools') format('svg');
|
||||
src: url('fonts/cptools.eot?nobkj');
|
||||
src: url('fonts/cptools.eot?nobkj#iefix') format('embedded-opentype'),
|
||||
url('fonts/cptools.ttf?nobkj') format('truetype'),
|
||||
url('fonts/cptools.woff?nobkj') format('woff'),
|
||||
url('fonts/cptools.svg?nobkj#cptools') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@ -26,6 +26,9 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.cptools-diagram:before {
|
||||
content: "\e921";
|
||||
}
|
||||
.cptools-drive:before {
|
||||
content: "\e920";
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* Open Sans @font-face kit */
|
||||
|
||||
@OpenSansPath: "/bower_components/open-sans-fontface/fonts";
|
||||
@OpenSansPath: "/components/open-sans-fontface/fonts";
|
||||
|
||||
/* BEGIN Light */
|
||||
@font-face {
|
||||
|
||||
BIN
customize.dist/images/opengraph_preview/og-diagram.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 16 KiB |
@ -8,7 +8,7 @@
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript></noscript>
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
define([
|
||||
'jquery',
|
||||
'chainpad-listmap',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/components/chainpad-crypto/crypto.js',
|
||||
'/common/common-util.js',
|
||||
'/common/outer/network-config.js',
|
||||
'/common/common-credential.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
'/common/common-realtime.js',
|
||||
'/common/common-constants.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/common-feedback.js',
|
||||
'/common/outer/local-store.js',
|
||||
'/customize/messages.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/components/nthen/index.js',
|
||||
'/common/outer/login-block.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/outer/http-command.js',
|
||||
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
'/bower_components/scrypt-async/scrypt-async.min.js', // better load speed
|
||||
'/components/tweetnacl/nacl-fast.min.js',
|
||||
'/components/scrypt-async/scrypt-async.min.js', // better load speed
|
||||
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
|
||||
Feedback, LocalStore, Messages, nThen, Block, Hash) {
|
||||
Feedback, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
|
||||
var Exports = {
|
||||
Cred: Cred,
|
||||
Block: Block,
|
||||
@ -99,7 +100,6 @@ define([
|
||||
opt.channelHex = parsed.channel;
|
||||
opt.keys = parsed.keys;
|
||||
opt.edPublic = blockInfo.edPublic;
|
||||
opt.User_name = blockInfo.User_name;
|
||||
return opt;
|
||||
};
|
||||
|
||||
@ -135,7 +135,7 @@ define([
|
||||
Exports.mergeAnonDrive = 1;
|
||||
};
|
||||
|
||||
Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, cb) {
|
||||
Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, onOTP, cb) {
|
||||
if (typeof(cb) !== 'function') { return; }
|
||||
|
||||
// Usernames are all lowercase. No going back on this one
|
||||
@ -173,26 +173,113 @@ define([
|
||||
// determine where a block for your set of keys would be stored
|
||||
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
|
||||
|
||||
// Check whether there is a block at that location
|
||||
Util.fetch(blockUrl, waitFor(function (err, block) {
|
||||
// if users try to log in or register, we must check
|
||||
// whether there is a block.
|
||||
var TOTP_prompt = function (err, cb) {
|
||||
onOTP(function (code) {
|
||||
ServerCommand(res.opt.blockKeys.sign, {
|
||||
command: 'TOTP_VALIDATE',
|
||||
code: code,
|
||||
// TODO optionally allow the user to specify a lifetime for this session?
|
||||
// this will require a little bit of server work
|
||||
// and more UI/UX:
|
||||
// ie. just a simple "remember me" checkbox?
|
||||
// allow them to specify a lifetime for the session?
|
||||
// "log me out after one day"?
|
||||
}, cb);
|
||||
}, false, err);
|
||||
};
|
||||
|
||||
// the block is only useful if it can be decrypted, though
|
||||
if (err) {
|
||||
console.log("no block found");
|
||||
return;
|
||||
}
|
||||
var done = waitFor();
|
||||
var responseToDecryptedBlock = function (response, cb) {
|
||||
response.arrayBuffer().then(arraybuffer => {
|
||||
arraybuffer = new Uint8Array(arraybuffer);
|
||||
var decryptedBlock = Block.decrypt(arraybuffer, blockKeys);
|
||||
if (!decryptedBlock) {
|
||||
console.error("BLOCK DECRYPTION ERROR");
|
||||
return void cb("BLOCK_DECRYPTION_ERROR");
|
||||
}
|
||||
cb(void 0, decryptedBlock);
|
||||
});
|
||||
};
|
||||
|
||||
var decryptedBlock = Block.decrypt(block, blockKeys);
|
||||
if (!decryptedBlock) {
|
||||
console.error("Found a login block but failed to decrypt");
|
||||
return;
|
||||
}
|
||||
var TOTP_response;
|
||||
nThen(function (w) {
|
||||
Util.getBlock(blockUrl, {
|
||||
// request the block without credentials
|
||||
}, w(function (err, response) {
|
||||
if (err === 401) {
|
||||
return void console.log("Block requires 2FA");
|
||||
}
|
||||
|
||||
//console.error(decryptedBlock);
|
||||
res.blockInfo = decryptedBlock;
|
||||
}));
|
||||
// Some other error?
|
||||
if (err) {
|
||||
console.error(err);
|
||||
w.abort();
|
||||
return void done();
|
||||
}
|
||||
|
||||
// If the block was returned without requiring authentication
|
||||
// then we can abort the subsequent steps of this nested nThen
|
||||
w.abort();
|
||||
|
||||
// decrypt the response and continue the normal procedure with its payload
|
||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
||||
if (err) {
|
||||
// if a block was present but you were not able to decrypt it...
|
||||
console.error(err);
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.blockInfo = decryptedBlock;
|
||||
done();
|
||||
});
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// if you're here then you need to request a JWT
|
||||
var done = w();
|
||||
var tries = 3;
|
||||
var ask = function () {
|
||||
if (!tries) {
|
||||
w.abort();
|
||||
waitFor.abort();
|
||||
return void cb('TOTP_ATTEMPTS_EXHAUSTED');
|
||||
}
|
||||
tries--;
|
||||
TOTP_prompt(tries !== 2, function (err, response) {
|
||||
// ask again until your number of tries are exhausted
|
||||
if (err) {
|
||||
console.error(err);
|
||||
console.log("Normal failure. Asking again...");
|
||||
return void ask();
|
||||
}
|
||||
if (!response || !response.bearer) {
|
||||
console.log(response);
|
||||
console.log("Unexpected failure. No bearer token. Asking again");
|
||||
return void ask();
|
||||
}
|
||||
console.log("Successfully retrieved a bearer token");
|
||||
res.TOTP_token = TOTP_response = response;
|
||||
done();
|
||||
});
|
||||
};
|
||||
ask();
|
||||
}).nThen(function (w) {
|
||||
Util.getBlock(blockUrl, TOTP_response, function (err, response) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
console.error(err);
|
||||
return void cb('BLOCK_ERROR_3');
|
||||
}
|
||||
|
||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
res.blockInfo = decryptedBlock;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).nThen(function (waitFor) {
|
||||
// we assume that if there is a block, it was created in a valid manner
|
||||
// so, just proceed to the next block which handles that stuff
|
||||
@ -275,7 +362,7 @@ define([
|
||||
Realtime.whenRealtimeSyncs(rt.realtime, function () {
|
||||
// the following stages are there to initialize a new drive
|
||||
// if you are registering
|
||||
LocalStore.login(res.userHash, res.userName, function () {
|
||||
LocalStore.login(res.userHash, undefined, res.userName, function () {
|
||||
setTimeout(function () { cb(void 0, res); });
|
||||
});
|
||||
});
|
||||
@ -348,7 +435,6 @@ define([
|
||||
}
|
||||
|
||||
if (!isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
LocalStore.setBlockHash(blockHash);
|
||||
waitFor.abort();
|
||||
if (shouldImport) {
|
||||
setMergeAnonDrive();
|
||||
@ -358,7 +444,11 @@ define([
|
||||
if (l) {
|
||||
localStorage.setItem(LS_LANG, l);
|
||||
}
|
||||
return void LocalStore.login(userHash, uname, function () {
|
||||
|
||||
if (res.TOTP_token && res.TOTP_token.bearer) {
|
||||
LocalStore.setSessionToken(res.TOTP_token.bearer);
|
||||
}
|
||||
return void LocalStore.login(undefined, blockHash, uname, function () {
|
||||
cb(void 0, res);
|
||||
});
|
||||
}
|
||||
@ -409,12 +499,13 @@ define([
|
||||
// Finally, create the login block for the object you just created.
|
||||
var toPublish = {};
|
||||
|
||||
toPublish[Constants.userNameKey] = uname;
|
||||
toPublish[Constants.userHashKey] = userHash;
|
||||
toPublish.edPublic = RT.proxy.edPublic;
|
||||
|
||||
var blockRequest = Block.serialize(JSON.stringify(toPublish), res.opt.blockKeys);
|
||||
rpc.writeLoginBlock(blockRequest, waitFor(function (e) {
|
||||
Block.writeLoginBlock({
|
||||
blockKeys: blockKeys,
|
||||
content: toPublish
|
||||
}, waitFor(function (e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
waitFor.abort();
|
||||
@ -431,8 +522,7 @@ define([
|
||||
}
|
||||
|
||||
console.log("blockInfo available at:", blockHash);
|
||||
LocalStore.setBlockHash(blockHash);
|
||||
LocalStore.login(userHash, uname, function () {
|
||||
LocalStore.login(undefined, blockHash, uname, function () {
|
||||
cb(void 0, res);
|
||||
});
|
||||
}));
|
||||
@ -458,7 +548,7 @@ define([
|
||||
};
|
||||
|
||||
var hashing;
|
||||
Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, testing, test) {
|
||||
Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, onOTP, testing, test) {
|
||||
if (hashing) { return void console.log("hashing is already in progress"); }
|
||||
hashing = true;
|
||||
|
||||
@ -483,7 +573,7 @@ define([
|
||||
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
|
||||
// after hashing the password
|
||||
window.setTimeout(function () {
|
||||
Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, function (err, result) {
|
||||
Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, onOTP, function (err, result) {
|
||||
var proxy;
|
||||
if (result) { proxy = result.proxy; }
|
||||
|
||||
@ -537,11 +627,9 @@ define([
|
||||
proxy[Constants.displayNameKey] = uname;
|
||||
}
|
||||
|
||||
if (result.blockHash) {
|
||||
LocalStore.setBlockHash(result.blockHash);
|
||||
}
|
||||
|
||||
LocalStore.login(result.userHash, result.userName, function () {
|
||||
var block = result.blockHash;
|
||||
var user = block ? undefined : result.userHash;
|
||||
LocalStore.login(user, block, result.userName, function () {
|
||||
setTimeout(function () { proceed(result); });
|
||||
});
|
||||
});
|
||||
|
||||
@ -95,7 +95,7 @@ define([
|
||||
return h('a', attrs, [icon, text]);
|
||||
};
|
||||
|
||||
Pages.versionString = "5.3.0";
|
||||
Pages.versionString = "5.4.0";
|
||||
|
||||
var customURLs = Pages.customURLs = {};
|
||||
(function () {
|
||||
|
||||
@ -35,6 +35,7 @@ define([
|
||||
};
|
||||
|
||||
return function () {
|
||||
document.title = Msg.homePage;
|
||||
var icons = [
|
||||
[ 'sheet', Msg.type.sheet],
|
||||
[ 'doc', Msg.type.doc],
|
||||
@ -43,7 +44,7 @@ define([
|
||||
[ 'kanban', Msg.type.kanban],
|
||||
[ 'code', Msg.type.code],
|
||||
[ 'form', Msg.type.form],
|
||||
[ 'whiteboard', Msg.type.whiteboard],
|
||||
[ 'diagram', Msg.type.diagram],
|
||||
[ 'slide', Msg.type.slide]
|
||||
].filter(function (x) {
|
||||
return isAvailableType(x[0]);
|
||||
|
||||
85
customize.dist/pages/recovery.js
Normal file
@ -0,0 +1,85 @@
|
||||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-interface.js',
|
||||
'/customize/messages.js',
|
||||
'/customize/pages.js'
|
||||
], function (Config, $, h, UI, Msg, Pages) {
|
||||
|
||||
return function () {
|
||||
document.title = Msg.recovery_header;
|
||||
|
||||
var frame = function (content) {
|
||||
return [
|
||||
h('div#cp-main', [
|
||||
Pages.infopageTopbar(),
|
||||
h('div.container.cp-container', [
|
||||
h('div.row.cp-page-title', h('h1', Msg.recovery_header)),
|
||||
].concat(content)),
|
||||
Pages.infopageFooter(),
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
return frame([
|
||||
h('div.row.cp-recovery-det', [
|
||||
h('div.hidden.col-md-3'),
|
||||
h('div#userForm.form-group.hidden.col-md-6', [
|
||||
h('div.cp-recovery-step.step1', [
|
||||
h('p', Msg.recovery_mfa_description),
|
||||
h('div.alert.alert-danger.wrong-cred.cp-hidden', Msg.login_noSuchUser),
|
||||
h('input.form-control#username', {
|
||||
type: 'text',
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'off',
|
||||
spellcheck: false,
|
||||
placeholder: Msg.login_username,
|
||||
autofocus: true,
|
||||
}),
|
||||
h('input.form-control#password', {
|
||||
type: 'password',
|
||||
placeholder: Msg.login_password,
|
||||
}),
|
||||
h('div.cp-recover-button',
|
||||
h('button.btn.btn-primary#cp-recover-login', Msg.continue)
|
||||
)
|
||||
]),
|
||||
h('div.cp-recovery-step.step2', { style: 'display: none;' }, [
|
||||
h('label', Msg.recovery_mfa_secret),
|
||||
h('input.form-control#mfarecovery', {
|
||||
type: 'text',
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'off',
|
||||
spellcheck: false,
|
||||
placeholder: Msg.recovery_mfa_secret_ph,
|
||||
autofocus: true,
|
||||
}),
|
||||
h('div.cp-recovery-forgot', [
|
||||
h('i.fa.fa-caret-right'),
|
||||
h('span', Msg.recovery_forgot)
|
||||
]),
|
||||
h('div.cp-recovery-alt', { style: 'display: none;' }, [
|
||||
UI.setHTML(h('div'),
|
||||
Msg._getKey('recovery_forgot_text', [Config.adminEmail || ''])),
|
||||
h('textarea.cp-recover-email', {readonly: 'readonly'}),
|
||||
h('button.btn.btn-secondary#mfacopyproof', Msg.copyToClipboard),
|
||||
]),
|
||||
h('div.cp-recover-button',
|
||||
h('button.btn.btn-primary#cp-recover', Msg.mfa_disable)
|
||||
)
|
||||
]),
|
||||
h('div.cp-recovery-step.step-info', { style: 'display: none;' }, [
|
||||
h('div.alert.alert-info.cp-hidden.disabled', Msg.recovery_mfa_disabled),
|
||||
h('div.alert.alert-danger.cp-hidden.unknown-error', Msg.recovery_mfa_error),
|
||||
]),
|
||||
]),
|
||||
h('div.hidden.col-md-3'),
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
font-size: large;
|
||||
|
||||
box-shadow: @alertify_box-shadow;
|
||||
border-radius: @variables_radius;
|
||||
&, &.default {
|
||||
// FIXME
|
||||
background: @cp_alertify-log-bg;
|
||||
@ -487,5 +488,32 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
// XXX this might not be the best place for this.
|
||||
// I just put it next to other "share" styles
|
||||
// --Aaron
|
||||
#cp-qr-container {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
display: inline-flex;
|
||||
padding: @alertify_padding-base;
|
||||
border-radius: @variables_radius_L;
|
||||
#cp-qr-blocker {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: @variables_radius_L;
|
||||
|
||||
margin: -@alertify_padding-base;
|
||||
padding-top: @alertify_padding-base * 2;
|
||||
text-align: center;
|
||||
|
||||
background: @cryptpad_color_brand;
|
||||
color: @cryptpad_text_col;
|
||||
font-weight: bold;
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,8 @@
|
||||
slide: #e57614;
|
||||
poll: #2c9e98;
|
||||
form: #2c9e98;
|
||||
whiteboard: #a72ba7;
|
||||
whiteboard: #8f40f5;
|
||||
diagram: #ce3ad3;
|
||||
kanban: #8C4;
|
||||
sheet: #40865c;
|
||||
doc: #5170B5;
|
||||
@ -59,6 +60,7 @@
|
||||
@cryptpad_color_red_fader: fade(@cryptpad_color_red, 15%);
|
||||
@cryptpad_color_warn_red: @cryptpad_color_red_fade;
|
||||
@cryptpad_color_dark_red: #9e0000;
|
||||
@cryptpad_color_mid_red: #FF8880;
|
||||
@cryptpad_color_light_red: #FFD4D4;
|
||||
@cryptpad_color_light_red_fade: fade(@cryptpad_color_light_red, 20%);
|
||||
@cryptpad_color_orange: #f49842;
|
||||
@ -180,6 +182,10 @@
|
||||
@cp_sidebar-left-active-fg: @cryptpad_color_grey_900;
|
||||
@cp_sidebar-hint: fade(@cryptpad_text_col, 80%);
|
||||
|
||||
// Settings
|
||||
@cp_settings_enabled: @cryptpad_color_light_green;
|
||||
@cp_settings_disabled: @cryptpad_color_mid_red;
|
||||
|
||||
// Drive
|
||||
@cp_drive-bg: @cp_sidebar-right-bg;
|
||||
@cp_drive-fg: @cp_sidebar-right-fg;
|
||||
|
||||
@ -11,7 +11,8 @@
|
||||
slide: #e57614;
|
||||
poll: #2c9e98;
|
||||
form: #2c9e98;
|
||||
whiteboard: #a72ba7;
|
||||
whiteboard: #8f40f5;
|
||||
diagram: #ce3ad3;
|
||||
kanban: #8C4;
|
||||
sheet: #40865c;
|
||||
doc: #5170B5;
|
||||
@ -59,6 +60,7 @@
|
||||
@cryptpad_color_red_fader: fade(@cryptpad_color_red, 15%);
|
||||
@cryptpad_color_warn_red: @cryptpad_color_red_fade;
|
||||
@cryptpad_color_dark_red: #9e0000;
|
||||
@cryptpad_color_mid_red: #FF8880;
|
||||
@cryptpad_color_light_red: #FFD4D4;
|
||||
@cryptpad_color_light_red_fade: fade(@cryptpad_color_light_red, 75%);
|
||||
@cryptpad_color_orange: #f49842;
|
||||
@ -179,6 +181,10 @@
|
||||
@cp_sidebar-left-active-fg: @cryptpad_color_grey_200;
|
||||
@cp_sidebar-hint: @cryptpad_color_grey_600;
|
||||
|
||||
// Settings
|
||||
@cp_settings_enabled: extract(@cp_palette-dark, 4);
|
||||
@cp_settings_disabled: @cryptpad_color_dark_red;
|
||||
|
||||
// Drive
|
||||
@cp_drive-bg: @cp_sidebar-right-bg;
|
||||
@cp_drive-fg: @cp_sidebar-right-fg;
|
||||
|
||||
@ -291,7 +291,14 @@
|
||||
margin-bottom: 3px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.droppable-tree-element () {
|
||||
.cp-app-drive-element-droppable {
|
||||
background-color: @cp_drive-droppable-bg !important;
|
||||
color: @cp_drive-droppable-fg !important;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-tree-docs {
|
||||
.droppable-tree-element();
|
||||
margin-top: 15px;
|
||||
//padding: 0 0 0 20px;
|
||||
padding: 0;
|
||||
@ -346,6 +353,7 @@
|
||||
border-radius: @variables_radius;
|
||||
box-shadow: @cryptpad_ui_shadow;
|
||||
.cp-app-drive-tree-root {
|
||||
.droppable-tree-element();
|
||||
.fa-trash-o {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
font-size: @colortheme_app-font-size;
|
||||
}
|
||||
&[readonly] {
|
||||
//margin-top:1rem;
|
||||
background-color: @cp_forms-readonly;
|
||||
border-color: @cp_forms-readonly-border;
|
||||
color: @cp_forms-fg;
|
||||
@ -53,6 +54,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&.mfa-enabled {
|
||||
span, i {
|
||||
color: @cp_settings_enabled;
|
||||
}
|
||||
}
|
||||
&.mfa-disabled {
|
||||
span, i {
|
||||
color: @cp_settings_disabled;
|
||||
}
|
||||
}
|
||||
|
||||
textarea, div.cp-textarea {
|
||||
padding: 8px;
|
||||
@ -101,12 +112,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
button.cp-button-confirm-placeholder:not(.new) {
|
||||
margin-bottom: 3px !important;
|
||||
}
|
||||
|
||||
button.btn {
|
||||
|
||||
background-color: @cp_buttons-cancel;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
@ -167,6 +174,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.danger-alt, &.btn-danger-alt, &.btn-danger-outline {
|
||||
border-color: @cp_buttons-red;
|
||||
color: @cp_buttons-red-text;
|
||||
@ -177,11 +185,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.primary, &.btn-primary, &.btn-success {
|
||||
&.primary, &.btn-primary, &.btn-success, &.disable-button {
|
||||
background-color: @cp_buttons-primary;
|
||||
color: @cp_buttons-primary-text;
|
||||
border-color: @cp_buttons-primary-border;
|
||||
font-weight: bold;
|
||||
&:hover, &:not(:disabled):active, &:focus {
|
||||
color: @cp_buttons-primary-text;
|
||||
border-color: @cp_buttons-primary-border;
|
||||
|
||||
@ -20,6 +20,11 @@
|
||||
.infopages_main () {
|
||||
--LessLoader_require: LessLoader_currentFile();
|
||||
}
|
||||
|
||||
.cp-loading-noscroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.html {
|
||||
.font_main();
|
||||
@infopages_infobar-height: 64px;
|
||||
@ -105,7 +110,7 @@ body.html {
|
||||
filter: @cp_static-img-invert-filter;
|
||||
}
|
||||
|
||||
button {
|
||||
button:not(.btn) {
|
||||
outline: none;
|
||||
background-color: @cp_buttons-primary;
|
||||
color: @cp_buttons-primary-text;
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
#cp-loading {
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.eot');
|
||||
src: url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.woff') format('woff'),
|
||||
url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.ttf') format('truetype'),
|
||||
url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.svg#OpenSansRegular') format('svg');
|
||||
src: url('/components/open-sans-fontface/fonts/Regular/OpenSans-Regular.eot');
|
||||
src: url('/components/open-sans-fontface/fonts/Regular/OpenSans-Regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('/components/open-sans-fontface/fonts/Regular/OpenSans-Regular.woff') format('woff'),
|
||||
url('/components/open-sans-fontface/fonts/Regular/OpenSans-Regular.ttf') format('truetype'),
|
||||
url('/components/open-sans-fontface/fonts/Regular/OpenSans-Regular.svg#OpenSansRegular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@ -18,6 +18,11 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@media(min-width:800px) {
|
||||
#cp-sidebarlayout-leftside {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
#cp-sidebarlayout-leftside {
|
||||
color: @cp_sidebar-left-fg;
|
||||
width: 250px;
|
||||
@ -98,6 +103,9 @@
|
||||
}
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.secret-code {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
[type="text"], [type="password"], button {
|
||||
vertical-align: middle;
|
||||
min-width: 40px;
|
||||
@ -127,11 +135,11 @@
|
||||
&>div {
|
||||
margin: 10px 0;
|
||||
}
|
||||
button.btn {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
//button.btn {
|
||||
// margin: 0 5px 0 0;
|
||||
//}
|
||||
span.cp-password-container {
|
||||
margin-bottom: 1px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: @browser_media-medium-screen) {
|
||||
|
||||
@ -53,6 +53,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-password-form {
|
||||
flex-flow: row !important;
|
||||
input:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.cp-container {
|
||||
padding-top: 3em;
|
||||
min-height: 66vh;
|
||||
|
||||
115
customize.dist/src/less2/pages/page-recovery.less
Normal file
@ -0,0 +1,115 @@
|
||||
@import (reference) "../include/infopages.less";
|
||||
@import (reference) "../include/colortheme-all.less";
|
||||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/checkmark.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
|
||||
&.cp-page-recovery {
|
||||
.infopages_main();
|
||||
.forms_main();
|
||||
|
||||
.alertify_main();
|
||||
.checkmark_main(20px);
|
||||
|
||||
.cp-container {
|
||||
.alert {
|
||||
font-size: @colortheme_app-font-size;
|
||||
}
|
||||
.form-group {
|
||||
.cp-recovery-desc {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cp-recovery-desc, .cp-recovery-step {
|
||||
width: 100%;
|
||||
}
|
||||
#register {
|
||||
&.btn {
|
||||
padding: .5rem .5rem;
|
||||
}
|
||||
margin-top: 16px;
|
||||
font-size: 1.25em;
|
||||
min-width: 30%;
|
||||
}
|
||||
}
|
||||
padding-bottom: 3em;
|
||||
min-height: 5vh;
|
||||
.cp-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.alertify {
|
||||
// workaround for alertify making empty p
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
nav .btn-danger {
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.cp-recovery-det {
|
||||
.cp-recover-button {
|
||||
text-align: right;
|
||||
}
|
||||
.cp-recovery-forgot {
|
||||
cursor: pointer;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
.cp-recovery-method {
|
||||
padding: 5px;
|
||||
border: 1px solid white;
|
||||
border-radius: 5px;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.cp-recover-email {
|
||||
height: 164px;
|
||||
overflow: scroll;
|
||||
}
|
||||
//for chrome,safari
|
||||
.cp-recover-email::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#userForm {
|
||||
padding: 15px;
|
||||
background-color: @cp_static-card-bg;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 100px;
|
||||
border-radius: @infopages-radius-L;
|
||||
.cp-shadow();
|
||||
.form-control {
|
||||
border-radius: @infopages-radius;
|
||||
color: @cryptpad_text_col;
|
||||
background-color: @cp_forms-bg;
|
||||
margin-bottom: 10px;
|
||||
&:focus {
|
||||
border-color: @cryptpad_color_brand;
|
||||
}
|
||||
.tools_placeholder-color();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,3 +18,4 @@ iframe-placeholder, #sbox-iframe, #sbox-secure-iframe {
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript></noscript>
|
||||
|
||||
@ -2,9 +2,9 @@ define([
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/customize/pages.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/components/nthen/index.js',
|
||||
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'css!/components/components-font-awesome/css/font-awesome.min.css',
|
||||
], function ($, h, Pages, nThen) {
|
||||
// we consider that there is no valid reason to load any of the info pages
|
||||
// in an iframe. abort everything if you detect that you are embedded.
|
||||
@ -41,11 +41,19 @@ $(function () {
|
||||
});
|
||||
}).nThen(function () {
|
||||
require([
|
||||
'/api/config',
|
||||
'/common/common-util.js',
|
||||
'optional!/api/instance',
|
||||
'less!/customize/src/less2/pages/page-' + pageName + '.less',
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'css!/components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'css!/customize/fonts/cptools/style.css'
|
||||
], function () {
|
||||
], function (ApiConfig, Util, Instance) {
|
||||
var $main = $(infoPage());
|
||||
var titleSuffix = (Util.find(Instance, ['name','default']) || '').trim();
|
||||
if (!titleSuffix || titleSuffix === ApiConfig.httpUnsafeOrigin) {
|
||||
titleSuffix = window.location.hostname;
|
||||
}
|
||||
document.title = document.title + ' - ' + titleSuffix;
|
||||
|
||||
$('#placeholder').remove();
|
||||
|
||||
@ -55,6 +63,8 @@ $(function () {
|
||||
require([ '/register/main.js' ], function () {});
|
||||
} else if (/^\/install\//.test(pathname)) {
|
||||
require([ '/install/main.js' ], function () {});
|
||||
} else if (/^\/recovery\//.test(pathname)) {
|
||||
require([ '/recovery/main.js' ], function () {});
|
||||
} else if (/^\/login\//.test(pathname)) {
|
||||
require([ '/login/main.js' ], function () {});
|
||||
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
|
||||
|
||||
28
docker-compose.yml
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
cryptpad:
|
||||
image: "cryptpad/cryptpad:version-5.4.0"
|
||||
hostname: cryptpad
|
||||
|
||||
environment:
|
||||
- CPAD_MAIN_DOMAIN=https://your-main-domain.com
|
||||
- CPAD_SANDBOX_DOMAIN=https://your-sandbox-domain.com
|
||||
- CPAD_CONF=/cryptpad/config/config.js
|
||||
|
||||
volumes:
|
||||
- ./data/blob:/cryptpad/blob
|
||||
- ./data/block:/cryptpad/block
|
||||
- ./customize:/cryptpad/customize
|
||||
- ./data/data:/cryptpad/data
|
||||
- ./data/files:/cryptpad/datastore
|
||||
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3001:3001"
|
||||
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1000000
|
||||
hard: 1000000
|
||||
31
docker-entrypoint.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#/bin/bash
|
||||
|
||||
## Required vars
|
||||
# CPAD_MAIN_DOMAIN
|
||||
# CPAD_SANDBOX_DOMAIN
|
||||
# CPAD_CONF
|
||||
|
||||
set -e
|
||||
|
||||
CPAD_HOME="/cryptpad"
|
||||
|
||||
if [ ! -f "$CPAD_CONF" ]; then
|
||||
echo -e "\n\
|
||||
#################################################################### \n\
|
||||
Warning: No config file provided for cryptpad \n\
|
||||
We will create a basic one for now but you should rerun this service \n\
|
||||
by providing a file with your settings \n\
|
||||
eg: docker run -v /path/to/config.js:/cryptpad/config/config.js \n\
|
||||
#################################################################### \n"
|
||||
|
||||
cp "$CPAD_HOME"/config/config.example.js "$CPAD_CONF"
|
||||
|
||||
sed -i -e "s@\(httpUnsafeOrigin:\).*[^,]@\1 '$CPAD_MAIN_DOMAIN'@" \
|
||||
-e "s@\(^ *\).*\(httpSafeOrigin:\).*[^,]@\1\2 '$CPAD_SANDBOX_DOMAIN'@" $CPAD_CONF
|
||||
fi
|
||||
|
||||
cd $CPAD_HOME
|
||||
npm run build
|
||||
|
||||
exec "$@"
|
||||
|
||||
@ -79,6 +79,7 @@ server {
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin "${allowed_origins}";
|
||||
add_header Access-Control-Allow-Credentials true;
|
||||
# add_header X-Frame-Options "SAMEORIGIN";
|
||||
|
||||
# Opt out of Google's FLoC Network
|
||||
@ -161,6 +162,11 @@ server {
|
||||
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
|
||||
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
|
||||
|
||||
# draw.io uses inline script tags in it's index.html. The hashes are added here.
|
||||
if ($uri ~ ^\/components\/drawio\/src\/main\/webapp\/index.html.*$) {
|
||||
set $scriptSrc "'self' 'sha256-6zAB96lsBZREqf0sT44BhH1T69sm7HrN34rpMOcWbNo=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: https://${main_domain}";
|
||||
}
|
||||
|
||||
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
|
||||
if ($unsafe) {
|
||||
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
|
||||
@ -173,7 +179,12 @@ server {
|
||||
# We prefer to serve static content from nginx directly and to leave the API server to handle
|
||||
# the dynamic content that only it can manage. This is primarily an optimization
|
||||
location ^~ /cryptpad_websocket {
|
||||
proxy_pass http://localhost:3000;
|
||||
# XXX
|
||||
# static assets like blobs and blocks are served by clustered workers in the API server
|
||||
# Websocket traffic still needs to be handled by the main process, which means it needs
|
||||
# to be hosted on a different port. By default 3003 will be used, though this is configurable
|
||||
# via config.websocketPort
|
||||
proxy_pass http://localhost:3003;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@ -212,10 +223,15 @@ server {
|
||||
add_header Cross-Origin-Embedder-Policy require-corp;
|
||||
}
|
||||
|
||||
# encrypted blobs are immutable and are thus cached for a year
|
||||
location ^~ /blob/ {
|
||||
# Requests for blobs and blocks are now proxied to the API server
|
||||
# This simplifies NGINX path configuration in the event they are being hosted in a non-standard location
|
||||
# or with odd unexpected permissions. Serving blobs in this manner also means that it will be possible to
|
||||
# enforce access control for them, though this is not yet implemented.
|
||||
# Access control (via TOTP 2FA) has been added to blocks, so they can be handled with the same directives.
|
||||
location ~ ^/(blob|block)/.*$ {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "${allowed_origins}";
|
||||
add_header 'Access-Control-Allow-Credentials' true;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
@ -223,36 +239,19 @@ server {
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Cache-Control max-age=31536000;
|
||||
add_header 'Access-Control-Allow-Origin' "${allowed_origins}";
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
|
||||
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
|
||||
try_files $uri =404;
|
||||
# Since we are proxying to the API server these headers can get duplicated
|
||||
# so we hide them
|
||||
proxy_hide_header 'X-Content-Type-Options';
|
||||
proxy_hide_header 'Access-Control-Allow-Origin';
|
||||
proxy_hide_header 'Permissions-Policy';
|
||||
proxy_hide_header 'X-XSS-Protection';^
|
||||
proxy_pass http://localhost:3000;
|
||||
}
|
||||
|
||||
# the "block-store" serves encrypted payloads containing users' drive keys
|
||||
# these payloads are unlocked via login credentials. They are mutable
|
||||
# and are thus never cached. They're small enough that it doesn't matter, in any case.
|
||||
location ^~ /block/ {
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Cache-Control max-age=0;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# This block provides an alternative means of loading content
|
||||
# otherwise only served via websocket. This is solely for debugging purposes,
|
||||
# and is thus not allowed by default.
|
||||
#location ^~ /datastore/ {
|
||||
#add_header Cache-Control max-age=0;
|
||||
#try_files $uri =404;
|
||||
#}
|
||||
|
||||
# The nodejs server has some built-in forwarding rules to prevent
|
||||
# URLs like /pad from resulting in a 404. This simply adds a trailing slash
|
||||
# to a variety of applications.
|
||||
location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert|checkup)$ {
|
||||
location ~ ^/(register|login|recovery|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert|checkup|diagram)$ {
|
||||
rewrite ^(.*)$ $1/ redirect;
|
||||
}
|
||||
|
||||
|
||||
16
lib/api.js
@ -6,6 +6,7 @@ const Decrees = require("./decrees");
|
||||
const nThen = require("nthen");
|
||||
const Fs = require("fs");
|
||||
const Path = require("path");
|
||||
const Nacl = require("tweetnacl/nacl-fast");
|
||||
|
||||
module.exports.create = function (Env) {
|
||||
var log = Env.Log;
|
||||
@ -21,6 +22,21 @@ nThen(function (w) {
|
||||
console.error(err);
|
||||
}
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// we assume the server has generated a secret used to validate JWT tokens
|
||||
if (typeof(Env.bearerSecret) === 'string') { return; }
|
||||
// if one does not exist, then create one and remember it
|
||||
// 256 bits
|
||||
var bearerSecret = Nacl.util.encodeBase64(Nacl.randomBytes(32));
|
||||
Env.Log.info("GENERATING_BEARER_SECRET", {});
|
||||
Decrees.write(Env, [
|
||||
'SET_BEARER_SECRET',
|
||||
[bearerSecret],
|
||||
'INTERNAL',
|
||||
+new Date()
|
||||
], w(function (err) {
|
||||
if (err) { throw err; }
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
var fullPath = Path.join(Env.paths.block, 'placeholder.txt');
|
||||
Fs.writeFile(fullPath, 'PLACEHOLDER\n', w());
|
||||
|
||||
76
lib/challenge-commands/base.js
Normal file
@ -0,0 +1,76 @@
|
||||
const Block = require("../commands/block");
|
||||
const MFA = require("../storage/mfa");
|
||||
const Util = require("../common-util");
|
||||
|
||||
const Commands = module.exports;
|
||||
|
||||
var isValidBlockId = Block.isValidBlockId;
|
||||
|
||||
// Read the MFA settings for the given public key
|
||||
const checkMFA = (Env, publicKey, cb) => {
|
||||
// Success if we can't get the MFA settings
|
||||
MFA.read(Env, publicKey, function (err, content) {
|
||||
if (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
Env.Log.error('TOTP_VALIDATE_MFA_READ', {
|
||||
error: err,
|
||||
publicKey: publicKey,
|
||||
});
|
||||
}
|
||||
return void cb();
|
||||
}
|
||||
|
||||
var parsed = Util.tryParse(content);
|
||||
if (!parsed) { return void cb(); }
|
||||
|
||||
cb("NOT_ALLOWED");
|
||||
});
|
||||
};
|
||||
|
||||
// Make sure the block is not protected by MFA but don't do anything else
|
||||
const check = Commands.MFA_CHECK = function (Env, body, cb) {
|
||||
var { publicKey } = body;
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
checkMFA(Env, publicKey, cb);
|
||||
};
|
||||
check.complete = function (Env, body, cb) { cb(); };
|
||||
|
||||
// Write a login block IFF
|
||||
// 1. You can sign for the block's public key
|
||||
// 2. the block is not protected by MFA
|
||||
// Note: the internal WRITE_LOGIN_BLOCK will check is you're allowed to create this block
|
||||
const writeBlock = Commands.WRITE_BLOCK = function (Env, body, cb) {
|
||||
const { publicKey, content } = body;
|
||||
|
||||
// they must provide a valid block public key
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
if (publicKey !== content.publicKey) { return void cb("INVALID_KEY"); }
|
||||
|
||||
// check MFA
|
||||
checkMFA(Env, publicKey, cb);
|
||||
};
|
||||
|
||||
writeBlock.complete = function (Env, body, cb) {
|
||||
const { content } = body;
|
||||
Block.writeLoginBlock(Env, content, cb);
|
||||
};
|
||||
|
||||
// Remove a login block IFF
|
||||
// 1. You can sign for the block's public key
|
||||
// 2. the block is not protected by MFA
|
||||
const removeBlock = Commands.REMOVE_BLOCK = function (Env, body, cb) {
|
||||
const { publicKey } = body;
|
||||
|
||||
// they must provide a valid block public key
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
|
||||
// check MFA
|
||||
checkMFA(Env, publicKey, cb);
|
||||
};
|
||||
|
||||
removeBlock.complete = function (Env, body, cb) {
|
||||
const { publicKey } = body;
|
||||
Block.removeLoginBlock(Env, publicKey, cb);
|
||||
};
|
||||
|
||||
|
||||
500
lib/challenge-commands/totp.js
Normal file
@ -0,0 +1,500 @@
|
||||
/* globals Buffer */
|
||||
const B32 = require("thirty-two");
|
||||
const OTP = require("notp");
|
||||
const nThen = require("nthen");
|
||||
const Util = require("../common-util");
|
||||
|
||||
const MFA = require("../storage/mfa");
|
||||
const Sessions = require("../storage/sessions");
|
||||
const BlockStore = require("../storage/block");
|
||||
const Block = require("../commands/block");
|
||||
|
||||
const Commands = module.exports;
|
||||
|
||||
var isString = s => typeof(s) === 'string';
|
||||
|
||||
// basic definition of what we'll accept as an OTP code
|
||||
// exactly six numerical digits
|
||||
var isValidOTP = otp => {
|
||||
return isString(otp) &&
|
||||
// in the future this could be updated to support 8 digits
|
||||
otp.length === 6 &&
|
||||
// \D is non-digit characters, so this tests that it is exclusively numeric
|
||||
!/\D/.test(otp);
|
||||
};
|
||||
|
||||
// basic definition of what we'll accept as a recovery key
|
||||
// 24 bytes encoded as b64 ==> 32 characters
|
||||
var isValidRecoveryKey = otp => {
|
||||
return isString(otp) &&
|
||||
// in the future this could be updated to support 8 digits
|
||||
otp.length === 32 &&
|
||||
// \D is non-digit characters, so this tests that it is exclusively numeric
|
||||
/[A-Za-z0-9+\/]{32}/.test(otp);
|
||||
};
|
||||
|
||||
// we'll only allow users to set up multi-factor auth
|
||||
// for keypairs they control which already have blocks
|
||||
// this check doesn't confirm that their id is valid base64
|
||||
// any attempt relying on this should fail when we can't decode
|
||||
// the id they provided.
|
||||
var isValidBlockId = Block.isValidBlockId;
|
||||
|
||||
// the base32 library can throw when decoding under various conditions.
|
||||
// we have some basic requirements for the length of base32 as well,
|
||||
// so we just do all the validation here. It either returns a buffer
|
||||
// of length 20 or undefined, so the caller can just check whether it's
|
||||
// falsey and otherwise assume it was well-formed
|
||||
// Length === 20 comes from the recommendation of 160 bits of entropy
|
||||
// in RFC4226 (https://www.rfc-editor.org/rfc/rfc4226#section-4)
|
||||
var decode32 = S => {
|
||||
let decoded;
|
||||
try {
|
||||
decoded = B32.decode(S);
|
||||
} catch (err) { return; }
|
||||
if (!(decoded instanceof Buffer) || decoded.length !== 20) { return; }
|
||||
return decoded;
|
||||
};
|
||||
|
||||
|
||||
// XXX Decide expire time
|
||||
// Allow user settings?
|
||||
var EXPIRATION = 7 * 24 * 3600 * 1000; // Sessions are valid 7 days
|
||||
|
||||
// Create a session with a token for the given public key
|
||||
const makeSession = (Env, publicKey, cb) => {
|
||||
const sessionId = Sessions.randomId();
|
||||
nThen(function (w) {
|
||||
// store the token
|
||||
Sessions.write(Env, publicKey, sessionId, JSON.stringify({
|
||||
mfa: {
|
||||
type: 'otp',
|
||||
exp: (+new Date()) + EXPIRATION
|
||||
}
|
||||
}), w(function (err) {
|
||||
if (err) {
|
||||
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
|
||||
error: Util.serializeError(err),
|
||||
publicKey: publicKey,
|
||||
sessionId: sessionId,
|
||||
});
|
||||
w.abort();
|
||||
return void cb("SESSION_WRITE_ERROR");
|
||||
}
|
||||
// else continue
|
||||
}));
|
||||
}).nThen(function () {
|
||||
cb(void 0, {
|
||||
bearer: sessionId,
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
// Read the MFA settings for the given public key
|
||||
const readMFA = (Env, publicKey, cb) => {
|
||||
// check that there is an MFA configuration for the given account
|
||||
MFA.read(Env, publicKey, function (err, content) {
|
||||
if (err) {
|
||||
Env.Log.error('TOTP_VALIDATE_MFA_READ', {
|
||||
error: err,
|
||||
publicKey: publicKey,
|
||||
});
|
||||
return void cb('NO_MFA_CONFIGURED');
|
||||
}
|
||||
|
||||
var parsed = Util.tryParse(content);
|
||||
if (!parsed) { return void cb("INVALID_CONFIGURATION"); }
|
||||
cb(undefined, parsed);
|
||||
});
|
||||
};
|
||||
// Check if an OTP code is valid against the provided secret
|
||||
const checkCode = (Env, secret, code, publicKey, _cb) => {
|
||||
const cb = Util.mkAsync(_cb);
|
||||
|
||||
let decoded = decode32(secret);
|
||||
if (!decoded) {
|
||||
Env.Log.error("TOTP_VALIDATE_INVALID_SECRET", {
|
||||
publicKey, // log the public key so the admin can investigate further
|
||||
// don't log the problematic secret directly as
|
||||
// logs are likely to be pasted in random places
|
||||
});
|
||||
return void cb("E_INVALID_SECRET");
|
||||
}
|
||||
|
||||
// validate the code
|
||||
var validated = OTP.totp.verify(code, decoded, {
|
||||
window: 1,
|
||||
});
|
||||
|
||||
if (!validated) {
|
||||
// I won't worry about logging these OTPs as they shouldn't leak any useful information
|
||||
Env.Log.error("TOTP_VALIDATE_BAD_OTP", {
|
||||
code,
|
||||
});
|
||||
return void cb("INVALID_OTP");
|
||||
}
|
||||
|
||||
// call back to indicate that their request was well-formed and valid
|
||||
cb();
|
||||
};
|
||||
|
||||
|
||||
// This command allows clients to configure TOTP as a second factor protecting
|
||||
// their login block IFF they:
|
||||
// 1. provide a sufficiently strong TOTP secret
|
||||
// 2. are able to produce a valid OTP code for that secret (indicating that their clock is sufficiently close to ours)
|
||||
// 3. such a login block actually exists
|
||||
// 4. are able to sign an arbitrary message for the login block's public key
|
||||
// 5. have not already configured TOTP protection for this account
|
||||
// (changing to a new secret can be done by disabling and re-enabling TOTP 2FA)
|
||||
const TOTP_SETUP = Commands.TOTP_SETUP = function (Env, body, cb) {
|
||||
const { publicKey, secret, code, contact } = body;
|
||||
|
||||
|
||||
// the client MUST provide an OTP code of the expected format
|
||||
// this doesn't check if it matches the secret and time, just that it's well-formed
|
||||
if (!isValidOTP(code)) { return void cb("E_INVALID"); }
|
||||
|
||||
// if they provide an (optional) point of contact as a recovery mechanism then it should be a string.
|
||||
// the intent is to allow to specify some side channel for those who inevitably lock themselves out
|
||||
// we should be able to use that to validate their identity.
|
||||
// I don't want to assume email, but limiting its length to 254 (the maximum email length) seems fair.
|
||||
if (contact && (!isString(contact) || contact.length > 254)) { return void cb("INVALID_CONTACT"); }
|
||||
|
||||
// Check that the provided public key is the expected format for a block
|
||||
if (!isValidBlockId(publicKey)) {
|
||||
return void cb("INVALID_KEY");
|
||||
}
|
||||
|
||||
// decode32 checks whether the secret decodes to a sufficiently long buffer
|
||||
var decoded = decode32(secret);
|
||||
if (!decoded) { return void cb('INVALID_SECRET'); }
|
||||
|
||||
// Reject attempts to setup TOTP if a record of their preferences already exists
|
||||
MFA.read(Env, publicKey, function (err) {
|
||||
// There **should be** an error here, because anything else
|
||||
// means that a record already exists
|
||||
// This may need to be adjusted as other methods of MFA are added
|
||||
if (!err) { return void cb("EEXISTS"); }
|
||||
|
||||
// if no MFA settings exist then we expect ENOENT
|
||||
// anything else indicates a problem and should result in rejection
|
||||
if (err.code !== 'ENOENT') { return void cb(err); }
|
||||
try {
|
||||
// allow for 30s of clock drift in either direction
|
||||
// returns an object ({ delta: 0 }) indicating the amount of clock drift
|
||||
// if successful, otherwise `null`
|
||||
var validated = OTP.totp.verify(code, decoded, {
|
||||
window: 1,
|
||||
});
|
||||
if (!validated) { return void cb("INVALID_OTP"); }
|
||||
cb();
|
||||
} catch (err2) {
|
||||
Env.Log.error('TOTP_SETUP_VERIFICATION_ERROR', {
|
||||
error: err2,
|
||||
});
|
||||
return void cb("INTERNAL_ERROR");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// The 'complete' step for TOTP_SETUP will only be called if the client
|
||||
// passed earlier validation and successfully signed the server's challenge.
|
||||
// There's still a little bit more to do and it could still fail.
|
||||
TOTP_SETUP.complete = function (Env, body, cb) {
|
||||
// the OTP code should have already been validated
|
||||
var { publicKey, secret, contact } = body;
|
||||
|
||||
// the device from which they configure MFA settings
|
||||
// is assumed to be safe, so we'll respond with a JWT token
|
||||
// the remainder of the setup is successfully completed.
|
||||
// Otherwise they would have to reauthenticate.
|
||||
// The session id is used as a reference to this particular session.
|
||||
nThen(function (w) {
|
||||
// confirm that the block exists
|
||||
BlockStore.check(Env, publicKey, w(function (err) {
|
||||
if (err) {
|
||||
Env.Log.error("TOTP_SETUP_NO_BLOCK", {
|
||||
publicKey,
|
||||
});
|
||||
w.abort();
|
||||
return void cb("NO_BLOCK");
|
||||
}
|
||||
// otherwise the block exists, continue
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// store the data you'll need in the future
|
||||
var data = {
|
||||
method: 'TOTP', // specify this so it's easier to add other methods later?
|
||||
secret: secret, // the 160 bit, base32-encoded secret that is used for OTP validation
|
||||
creation: new Date(), // the moment at which the MFA was configured
|
||||
};
|
||||
|
||||
if (isString(contact)) {
|
||||
// 'contact' is an arbitary (and optional) string for manual recovery from 2FA auth fails
|
||||
// it should already be validated
|
||||
data.contact = contact;
|
||||
}
|
||||
|
||||
// We attempt to store a record of the above preferences
|
||||
// if it fails then we abort and inform the client of an error.
|
||||
MFA.write(Env, publicKey, JSON.stringify(data), w(function (err) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
Env.Log.error("TOTP_SETUP_STORAGE_FAILURE", {
|
||||
publicKey: publicKey,
|
||||
error: err,
|
||||
});
|
||||
return void cb('STORAGE_FAILURE');
|
||||
}
|
||||
// otherwise continue
|
||||
}));
|
||||
}).nThen(function () {
|
||||
// we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret.
|
||||
// we attempt to create a session as a matter of convenience - so if it fails
|
||||
// that just means they'll be forced to authenticate
|
||||
makeSession(Env, publicKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
// This command is somewhat simpler than TOTP_SETUP
|
||||
// Issue a client a JWT which will allow them to access a login block IFF:
|
||||
// 1. That login block exists
|
||||
// 2. That login block is protected by TOTP 2FA
|
||||
// 3. They can produce a valid OTP for that block's TOTP secret
|
||||
// 4. They can sign for the block's public key
|
||||
const validate = Commands.TOTP_VALIDATE = function (Env, body, cb) {
|
||||
var { publicKey, code } = body;
|
||||
|
||||
// they must provide a valid OTP code
|
||||
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||
|
||||
// they must provide a valid block public key
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
|
||||
var secret;
|
||||
nThen(function (w) {
|
||||
// check that there is an MFA configuration for the given account
|
||||
readMFA(Env, publicKey, w(function (err, content) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
secret = content.secret;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
checkCode(Env, secret, code, publicKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
validate.complete = function (Env, body, cb) {
|
||||
/*
|
||||
if they are here then they:
|
||||
|
||||
1. have a valid block configured with TOTP-based 2FA
|
||||
2. were able to provide a valid TOTP for that block's secret
|
||||
3. were able to sign their messages for the block's public key
|
||||
|
||||
So, we should:
|
||||
|
||||
1. instanciate a session for them by generating and storing a token for their public key
|
||||
2. send them the token
|
||||
|
||||
*/
|
||||
var { publicKey } = body;
|
||||
makeSession(Env, publicKey, cb);
|
||||
};
|
||||
|
||||
// Same as TOTP_VALIDATE but without making a session at the end
|
||||
const check = Commands.TOTP_CHECK = function (Env, body, cb) {
|
||||
var { publicKey, auth } = body;
|
||||
const code = auth;
|
||||
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
var secret;
|
||||
nThen(function (w) {
|
||||
readMFA(Env, publicKey, w(function (err, content) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
secret = content.secret;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
checkCode(Env, secret, code, publicKey, cb);
|
||||
});
|
||||
};
|
||||
check.complete = function (Env, body, cb) { cb(); };
|
||||
|
||||
|
||||
// Revoke a client TOTP secret which will allow them to disable TOTP for a login block IFF:
|
||||
// 1. That login block exists
|
||||
// 2. That login block is protected by TOTP 2FA
|
||||
// 3. They can produce a valid OTP for that block's TOTP secret
|
||||
// 4. They can sign for the block's public key
|
||||
const revoke = Commands.TOTP_REVOKE = function (Env, body, cb) {
|
||||
var { publicKey, code, recoveryKey } = body;
|
||||
|
||||
// they must provide a valid OTP code
|
||||
if (!isValidOTP(code) && !isValidRecoveryKey(recoveryKey)) { return void cb('E_INVALID'); }
|
||||
|
||||
// they must provide a valid block public key
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
|
||||
var secret, recoveryStored;
|
||||
nThen(function (w) {
|
||||
// check that there is an MFA configuration for the given account
|
||||
readMFA(Env, publicKey, w(function (err, content) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
secret = content.secret;
|
||||
recoveryStored = content.contact;
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
if (!recoveryKey) { return; }
|
||||
w.abort();
|
||||
if (!/^secret:/.test(recoveryStored)) {
|
||||
return void cb("E_NO_RECOVERY_KEY");
|
||||
}
|
||||
recoveryStored = recoveryStored.slice(7);
|
||||
if (recoveryKey !== recoveryStored) {
|
||||
return void cb("E_WRONG_RECOVERY_KEY");
|
||||
}
|
||||
cb();
|
||||
}).nThen(function () {
|
||||
checkCode(Env, secret, code, publicKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
revoke.complete = function (Env, body, cb) {
|
||||
/*
|
||||
if they are here then they:
|
||||
|
||||
1. have a valid block configured with TOTP-based 2FA
|
||||
2. were able to provide a valid TOTP for that block's secret
|
||||
3. were able to sign their messages for the block's public key
|
||||
|
||||
So, we should:
|
||||
|
||||
1. Revoke the TOTP authentication for their block
|
||||
2. Remove all existing sessions
|
||||
*/
|
||||
var { publicKey } = body;
|
||||
MFA.revoke(Env, publicKey, cb);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Write a login block using an existing OTP block IFF
|
||||
// 1. You can sign for the block's public key
|
||||
// 2. You have a proof for the old block
|
||||
// 3. The old block is OTP protected
|
||||
// 4. The OTP code is valid
|
||||
// Note: this is used when users change their password
|
||||
const writeBlock = Commands.TOTP_WRITE_BLOCK = function (Env, body, cb) {
|
||||
const { publicKey, content } = body;
|
||||
const code = content.auth;
|
||||
const registrationProof = content.registrationProof;
|
||||
|
||||
// they must provide a valid block public key
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
if (publicKey !== content.publicKey) { return void cb("INVALID_KEY"); }
|
||||
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||
if (!registrationProof) { return void cb('MISSING_ANCESTOR'); }
|
||||
|
||||
let secret;
|
||||
let oldKey;
|
||||
nThen(function (w) {
|
||||
Block.validateAncestorProof(Env, registrationProof, w((err, provenKey) => {
|
||||
if (err || !provenKey) {
|
||||
w.abort();
|
||||
return void cb('INVALID_ANCESTOR');
|
||||
}
|
||||
oldKey = provenKey;
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// check that there is an MFA configuration for the ancestor account
|
||||
readMFA(Env, oldKey, w(function (err, content) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
secret = content.secret;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
// check that the OTP code is valid
|
||||
checkCode(Env, secret, code, oldKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
writeBlock.complete = function (Env, body, cb) {
|
||||
const { publicKey, content } = body;
|
||||
nThen(function (w) {
|
||||
// Write new block
|
||||
Block.writeLoginBlock(Env, content, w((err) => {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb("BLOCK_WRITE_ERROR");
|
||||
}
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// Copy MFA settings
|
||||
const proof = Util.tryParse(content.registrationProof);
|
||||
const oldKey = proof && proof[0];
|
||||
if (!oldKey) {
|
||||
w.abort();
|
||||
return void cb('INVALID_ANCESTOR');
|
||||
}
|
||||
MFA.copy(Env, oldKey, publicKey, w());
|
||||
}).nThen(function () {
|
||||
// Create a session for the current user
|
||||
makeSession(Env, publicKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
// Remove a login block IFF
|
||||
// 1. You can sign for the block's public key
|
||||
const removeBlock = Commands.TOTP_REMOVE_BLOCK = function (Env, body, cb) {
|
||||
const { publicKey, auth } = body;
|
||||
const code = auth;
|
||||
|
||||
// they must provide a valid block public key
|
||||
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||
|
||||
let secret;
|
||||
nThen(function (w) {
|
||||
// check that there is an MFA configuration for this block
|
||||
readMFA(Env, publicKey, w(function (err, content) {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
secret = content.secret;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
// check that the OTP code is valid
|
||||
checkCode(Env, secret, code, publicKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
removeBlock.complete = function (Env, body, cb) {
|
||||
const { publicKey } = body;
|
||||
nThen(function (w) {
|
||||
// Remove the block
|
||||
Block.removeLoginBlock(Env, publicKey, w((err) => {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
}));
|
||||
}).nThen(() => {
|
||||
// Delete the MFA settings and sessions
|
||||
MFA.revoke(Env, publicKey, cb);
|
||||
});
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ const Pinning = require("./pin-rpc");
|
||||
const Core = require("./core");
|
||||
const Channel = require("./channel");
|
||||
const BlockStore = require("../storage/block");
|
||||
const MFA = require("../storage/mfa");
|
||||
|
||||
var Fs = require("fs");
|
||||
|
||||
@ -498,6 +499,19 @@ var getDocumentStatus = function (Env, Server, cb, data) {
|
||||
}
|
||||
response.archived = result;
|
||||
}));
|
||||
MFA.read(Env, id, w(function (err, v) {
|
||||
if (err === 'ENOENT') {
|
||||
response.totp = 'DISABLED';
|
||||
} else if (v) {
|
||||
var parsed = Util.tryParse(v);
|
||||
response.totp = {
|
||||
enabled: true,
|
||||
recovery: parsed.contact && parsed.contact.split(':')[0]
|
||||
};
|
||||
} else {
|
||||
response.totp = err;
|
||||
}
|
||||
}));
|
||||
}).nThen(function () {
|
||||
cb(void 0, response);
|
||||
});
|
||||
@ -539,6 +553,12 @@ var getDocumentStatus = function (Env, Server, cb, data) {
|
||||
});
|
||||
};
|
||||
|
||||
var disableMFA = function (Env, Server, cb, data) {
|
||||
var id = Array.isArray(data) && data[1];
|
||||
if (typeof(id) !== 'string' || id.length !== 44) { return void cb("EINVAL"); }
|
||||
MFA.revoke(Env, id, cb);
|
||||
};
|
||||
|
||||
var getPinList = function (Env, Server, cb, data) {
|
||||
var key = Array.isArray(data) && data[1];
|
||||
if (!isValidKey(key)) { return void cb("EINVAL"); }
|
||||
@ -746,6 +766,8 @@ var commands = {
|
||||
GET_LAST_CHANNEL_TIME: getLastChannelTime,
|
||||
GET_DOCUMENT_STATUS: getDocumentStatus,
|
||||
|
||||
DISABLE_MFA: disableMFA,
|
||||
|
||||
GET_PIN_LIST: getPinList,
|
||||
GET_PIN_HISTORY: getPinHistory,
|
||||
ARCHIVE_PIN_LOG: archivePinLog,
|
||||
|
||||
@ -6,6 +6,11 @@ const nThen = require("nthen");
|
||||
const Util = require("../common-util");
|
||||
const BlockStore = require("../storage/block");
|
||||
|
||||
var isString = s => typeof(s) === 'string';
|
||||
Block.isValidBlockId = id => {
|
||||
return id && isString(id) && id.length === 44;
|
||||
};
|
||||
|
||||
/*
|
||||
We assume that the server is secured against MitM attacks
|
||||
via HTTPS, and that malicious actors do not have code execution
|
||||
@ -98,33 +103,24 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
|
||||
}
|
||||
};
|
||||
|
||||
Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
|
||||
Block.writeLoginBlock = function (Env, msg, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
var publicKey = msg[0];
|
||||
var signature = msg[1];
|
||||
var block = msg[2];
|
||||
var registrationProof = msg[3];
|
||||
var previousKey;
|
||||
const { publicKey, signature, ciphertext, registrationProof } = msg;
|
||||
|
||||
var previousKey;
|
||||
var validatedBlock, path;
|
||||
nThen(function (w) {
|
||||
if (Util.escapeKeyCharacters(publicKey) !== safeKey) {
|
||||
w.abort();
|
||||
return void cb("INCORRECT_KEY");
|
||||
}
|
||||
}).nThen(function (w) {
|
||||
if (!Env.restrictRegistration) { return; }
|
||||
if (!registrationProof) {
|
||||
// we allow users with existing blocks to create new ones
|
||||
// call back with error if registration is restricted and no proof of an existing block was provided
|
||||
w.abort();
|
||||
Env.Log.info("BLOCK_REJECTED_REGISTRATION", {
|
||||
safeKey: safeKey,
|
||||
publicKey: publicKey,
|
||||
});
|
||||
return cb("E_RESTRICTED");
|
||||
}
|
||||
Env.validateAncestorProof(registrationProof, w(function (err, provenKey) {
|
||||
Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) {
|
||||
if (err || !provenKey) { // double check that a key was validated
|
||||
w.abort();
|
||||
Env.Log.warn('BLOCK_REJECTED_INVALID_ANCESTOR', {
|
||||
@ -135,7 +131,7 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
|
||||
previousKey = provenKey;
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
Env.validateLoginBlock(publicKey, signature, block, w(function (e, _validatedBlock) {
|
||||
Block.validateLoginBlock(Env, publicKey, signature, ciphertext, w(function (e, _validatedBlock) {
|
||||
if (e) {
|
||||
w.abort();
|
||||
return void cb(e);
|
||||
@ -156,7 +152,6 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
|
||||
}
|
||||
BlockStore.write(Env, publicKey, buffer, function (err) {
|
||||
Env.Log.info('BLOCK_WRITE_BY_OWNER', {
|
||||
safeKey: safeKey,
|
||||
blockId: publicKey,
|
||||
isChange: Boolean(registrationProof),
|
||||
previousKey: previousKey,
|
||||
@ -167,8 +162,6 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
|
||||
});
|
||||
};
|
||||
|
||||
const DELETE_BLOCK = Nacl.util.encodeBase64(Nacl.util.decodeUTF8('DELETE_BLOCK'));
|
||||
|
||||
/*
|
||||
When users write a block, they upload the block, and provide
|
||||
a signature proving that they deserve to be able to write to
|
||||
@ -179,28 +172,15 @@ const DELETE_BLOCK = Nacl.util.encodeBase64(Nacl.util.decodeUTF8('DELETE_BLOCK')
|
||||
information, we can just sign some constant and use that as proof.
|
||||
|
||||
*/
|
||||
Block.removeLoginBlock = function (Env, safeKey, msg, _cb) {
|
||||
Block.removeLoginBlock = function (Env, publicKey, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
|
||||
var publicKey = msg[0];
|
||||
var signature = msg[1];
|
||||
|
||||
nThen(function (w) {
|
||||
if (Util.escapeKeyCharacters(publicKey) !== safeKey) {
|
||||
w.abort();
|
||||
return void cb("INCORRECT_KEY");
|
||||
}
|
||||
}).nThen(function () {
|
||||
Env.validateLoginBlock(publicKey, signature, DELETE_BLOCK, function (e) {
|
||||
if (e) { return void cb(e); }
|
||||
BlockStore.archive(Env, publicKey, function (err) {
|
||||
Env.Log.info('ARCHIVAL_BLOCK_BY_OWNER_RPC', {
|
||||
publicKey: publicKey,
|
||||
status: err? String(err): 'SUCCESS',
|
||||
});
|
||||
cb(err);
|
||||
});
|
||||
BlockStore.archive(Env, publicKey, function (err) {
|
||||
Env.Log.info('ARCHIVAL_BLOCK_BY_OWNER_RPC', {
|
||||
publicKey: publicKey,
|
||||
status: err? String(err): 'SUCCESS',
|
||||
});
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -49,6 +49,9 @@ SET_INSTANCE_DESCRIPTION
|
||||
SET_INSTANCE_NAME
|
||||
SET_INSTANCE_NOTICE
|
||||
|
||||
// bearer secret
|
||||
SET_BEARER_SECRET
|
||||
|
||||
NOT IMPLEMENTED:
|
||||
|
||||
// RESTRICTED REGISTRATION
|
||||
@ -348,6 +351,17 @@ commands.ADD_ADMIN_KEY = function (Env, args) {
|
||||
return true;
|
||||
};
|
||||
|
||||
commands.SET_BEARER_SECRET = function (Env, args) {
|
||||
if (!args_isString(args) || args.length !== 1 || !args[0]) {
|
||||
throw new Error("INVALID_ARGS");
|
||||
}
|
||||
|
||||
var secret = args[0];
|
||||
if (secret === Env.bearerSecret) { return false; }
|
||||
Env.bearerSecret = secret;
|
||||
return true;
|
||||
};
|
||||
|
||||
// [<command>, <args>, <author>, <time>]
|
||||
var handleCommand = Decrees.handleCommand = function (Env, line) {
|
||||
var command = line[0];
|
||||
@ -357,7 +371,12 @@ var handleCommand = Decrees.handleCommand = function (Env, line) {
|
||||
throw new Error("DECREE_UNSUPPORTED_COMMAND");
|
||||
}
|
||||
|
||||
return commands[command](Env, args);
|
||||
var outcome = commands[command](Env, args);
|
||||
if (outcome) {
|
||||
// trigger Env change event...
|
||||
Env.envUpdated.fire();
|
||||
}
|
||||
return outcome;
|
||||
};
|
||||
|
||||
Decrees.createLineHandler = function (Env) {
|
||||
|
||||
@ -26,7 +26,7 @@ Default.commonCSP = function (Env) {
|
||||
if you are deploying to production, you'll probably want to remove
|
||||
the ws://* directive
|
||||
*/
|
||||
"connect-src 'self' blob: " + (/^https:/.test(domain)? 'wss:': domain.replace('http://', 'ws://')) + ' ' + domain + sandbox + accounts_api,
|
||||
"connect-src 'self' localhost blob: " + (/^https:/.test(domain)? 'wss:': domain.replace('http://', 'ws://')) + ' ' + domain + sandbox + accounts_api,
|
||||
|
||||
// data: is used by codemirror
|
||||
"img-src 'self' data: blob:" + domain,
|
||||
@ -47,6 +47,10 @@ Default.padContentSecurity = function (Env) {
|
||||
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
Default.diagramContentSecurity = function (Env) {
|
||||
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'sha256-6zAB96lsBZREqf0sT44BhH1T69sm7HrN34rpMOcWbNo=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
Default.httpHeaders = function (Env) {
|
||||
return {
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
@ -65,6 +69,12 @@ Default.mainPages = function () {
|
||||
];
|
||||
};
|
||||
|
||||
/* The recommmended minimum Node.js version
|
||||
* ideally managed using NVM and not your system's
|
||||
* package manager, which usually provides a very outdated version
|
||||
*/
|
||||
Default.recommendedVersion = [16,14,2];
|
||||
|
||||
/* By default the CryptPad server will run scheduled tasks every five minutes
|
||||
* If you want to run scheduled tasks in a separate process (like a crontab)
|
||||
* you can disable this behaviour by setting the following value to true
|
||||
|
||||
84
lib/env.js
@ -11,6 +11,7 @@ const Core = require("./commands/core");
|
||||
const Quota = require("./commands/quota");
|
||||
const Util = require("./common-util");
|
||||
const Package = require("../package.json");
|
||||
const Default = require("./defaults");
|
||||
const Path = require("path");
|
||||
|
||||
const Nacl = require("tweetnacl/nacl-fast");
|
||||
@ -31,6 +32,7 @@ var deriveSandboxOrigin = function (unsafe, port) {
|
||||
};
|
||||
|
||||
var isRecentVersion = function () {
|
||||
var R = Default.recommendedVersion;
|
||||
var V = process.version;
|
||||
if (typeof(V) !== 'string') { return false; }
|
||||
var parts = V.replace(/^v/, '').split('.').map(Number);
|
||||
@ -38,13 +40,13 @@ var isRecentVersion = function () {
|
||||
if (!parts.every(n => typeof(n) === 'number' && !isNaN(n))) {
|
||||
return false;
|
||||
}
|
||||
if (parts[0] < 16) { return false; }
|
||||
if (parts[0] > 16) { return true; }
|
||||
if (parts[0] < R[0]) { return false; }
|
||||
if (parts[0] > R[0]) { return true; }
|
||||
|
||||
// v16
|
||||
if (parts[1] < 14) { return false; }
|
||||
if (parts[1] > 14) { return true; }
|
||||
if (parts[2] >= 2) { return true; }
|
||||
if (parts[1] < R[1]) { return false; }
|
||||
if (parts[1] > R[1]) { return true; }
|
||||
if (parts[2] >= R[2]) { return true; }
|
||||
|
||||
return false;
|
||||
};
|
||||
@ -65,6 +67,10 @@ module.exports.create = function (config) {
|
||||
httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin);
|
||||
}
|
||||
|
||||
if (typeof(config.websocketPort) !== 'number') {
|
||||
config.websocketPort = 3003;
|
||||
}
|
||||
|
||||
var permittedEmbedders = config.permittedEmbedders;
|
||||
if (typeof(permittedEmbedders) === 'string') {
|
||||
permittedEmbedders = permittedEmbedders.trim();
|
||||
@ -73,11 +79,16 @@ module.exports.create = function (config) {
|
||||
const curve = Nacl.box.keyPair();
|
||||
|
||||
const Env = {
|
||||
logFeedback: Boolean(config.logFeedback),
|
||||
mainPages: config.mainPages || Default.mainPages(),
|
||||
|
||||
protocol: new URL(httpUnsafeOrigin).protocol,
|
||||
|
||||
fileHost: config.fileHost || undefined,
|
||||
NO_SANDBOX: NO_SANDBOX,
|
||||
httpSafePort: httpSafePort,
|
||||
websocketPort: config.websocketPort,
|
||||
|
||||
accounts_api: config.accounts_api || undefined, // this simplifies integration with an accounts page
|
||||
|
||||
shouldUpdateNode: !isRecentVersion(),
|
||||
@ -106,15 +117,9 @@ module.exports.create = function (config) {
|
||||
apiHeadersCache: undefined,
|
||||
|
||||
flushCache: function () {
|
||||
Env.configCache = {};
|
||||
Env.broadcastCache = {};
|
||||
|
||||
Env.officeHeadersCache = undefined;
|
||||
Env.standardHeadersCache = undefined;
|
||||
Env.apiHeadersCache = undefined;
|
||||
|
||||
Env.FRESH_KEY = +new Date();
|
||||
if (!(Env.DEV_MODE || Env.FRESH_MODE)) { Env.FRESH_MODE = true; }
|
||||
Env.cacheFlushed.fire();
|
||||
if (!Env.Log) { return; }
|
||||
Env.Log.info("UPDATING_FRESH_KEY", Env.FRESH_KEY);
|
||||
},
|
||||
@ -211,17 +216,29 @@ module.exports.create = function (config) {
|
||||
// but it is referenced in Quota
|
||||
domain: config.domain,
|
||||
|
||||
maxWorkers: config.maxWorkers,
|
||||
maxWorkers: undefined,
|
||||
disableIntegratedTasks: config.disableIntegratedTasks || false,
|
||||
disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction,
|
||||
lastEviction: +new Date(),
|
||||
evictionReport: {},
|
||||
commandTimers: {},
|
||||
|
||||
// initialized as undefined
|
||||
bearerSecret: void 0,
|
||||
curvePrivate: curve.secretKey,
|
||||
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
|
||||
|
||||
selfDestructTo: {},
|
||||
};
|
||||
|
||||
(function () {
|
||||
var max = config.maxWorkers;
|
||||
// if the supplied value is not a positive number, leave maxWorkers undefined
|
||||
// one worker will be created for each CPU core
|
||||
if (typeof(max) !== 'number' || isNaN(max) || max < 1) { return; }
|
||||
Env.maxWorkers = max;
|
||||
}());
|
||||
|
||||
(function () {
|
||||
// mode can be FRESH (default), DEV, or PACKAGE
|
||||
if (process.env.PACKAGE) {
|
||||
@ -323,6 +340,7 @@ module.exports.create = function (config) {
|
||||
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
|
||||
paths.blob = keyOrDefaultString('blobPath', './blob');
|
||||
paths.decree = keyOrDefaultString('decreePath', './data/');
|
||||
paths.base = keyOrDefaultString('base', './data');
|
||||
paths.archive = keyOrDefaultString('archivePath', './data/archive');
|
||||
paths.task = keyOrDefaultString('taskPath', './tasks');
|
||||
|
||||
@ -342,5 +360,45 @@ module.exports.create = function (config) {
|
||||
console.error("Can't parse admin keys. Please update or fix your config.js file!");
|
||||
}
|
||||
|
||||
Env.envUpdated = Util.mkEvent();
|
||||
Env.cacheFlushed = Util.mkEvent();
|
||||
|
||||
return Env;
|
||||
};
|
||||
|
||||
// don't serialize these things
|
||||
const BAD = [
|
||||
'Log',
|
||||
'envUpdated',
|
||||
'cacheFlushed',
|
||||
'evictionReports',
|
||||
'commandTimers',
|
||||
'metadata_cache',
|
||||
'channel_cache',
|
||||
'cache_checks',
|
||||
'intervals',
|
||||
'Sessions',
|
||||
'netfluxUsers',
|
||||
'limits',
|
||||
'customLimits',
|
||||
'scheduleDecree',
|
||||
|
||||
'httpServer',
|
||||
|
||||
'pinStore',
|
||||
'msgStore',
|
||||
'store',
|
||||
'blobStore',
|
||||
];
|
||||
|
||||
module.exports.serialize = function (Env) {
|
||||
return JSON.stringify(Env, function (key, value) {
|
||||
if (value === Env) { return value; }
|
||||
if (BAD.includes(key)) { return; }
|
||||
|
||||
if (typeof(value) === 'function') { return; }
|
||||
//console.log('serializing', { key, value, });
|
||||
if (Util.isCircular(value)) { return; }
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
@ -63,6 +63,16 @@ module.exports.create = function (Env, cb) {
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
|
||||
if (metadata && metadata.selfdestruct && metadata.selfdestruct !== Env.id) {
|
||||
HK.expireChannel(Env, channelName);
|
||||
return void cb('ESELFDESTRUCT');
|
||||
}
|
||||
|
||||
if (Env.selfDestructTo && Env.selfDestructTo[channelName]) {
|
||||
clearTimeout(Env.selfDestructTo[channelName]);
|
||||
}
|
||||
|
||||
if (!metadata || (metadata && !metadata.restricted)) {
|
||||
// the channel doesn't have metadata, or it does and it's not restricted
|
||||
// either way, let them join.
|
||||
|
||||
@ -40,6 +40,9 @@ const ADMIN_CHANNEL_LENGTH = HK.ADMIN_CHANNEL_LENGTH = 33;
|
||||
// with a 34 character id
|
||||
const EPHEMERAL_CHANNEL_LENGTH = HK.EPHEMERAL_CHANNEL_LENGTH = 34;
|
||||
|
||||
// Temporary channels are archived X ms after everyone has left them
|
||||
const TEMPORARY_CHANNEL_LIFETIME = 30 * 1000;
|
||||
|
||||
const tryParse = HK.tryParse = function (Env, str) {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
@ -120,7 +123,7 @@ var CHECKPOINT_PATTERN = /^cp\|(([A-Za-z0-9+\/=]+)\|)?/;
|
||||
/* expireChannel is here to clean up channels that should have been removed
|
||||
but for some reason are still present
|
||||
*/
|
||||
const expireChannel = function (Env, channel) {
|
||||
const expireChannel = HK.expireChannel = function (Env, channel) {
|
||||
return void Env.store.archiveChannel(channel, function (err) {
|
||||
Env.Log.info("ARCHIVAL_CHANNEL_BY_HISTORY_KEEPER_EXPIRATION", {
|
||||
channelId: channel,
|
||||
@ -133,8 +136,14 @@ const expireChannel = function (Env, channel) {
|
||||
* cleans up memory structures which are managed entirely by the historyKeeper
|
||||
*/
|
||||
const dropChannel = HK.dropChannel = function (Env, chanName) {
|
||||
let meta = Env.metadata_cache[chanName];
|
||||
delete Env.metadata_cache[chanName];
|
||||
delete Env.channel_cache[chanName];
|
||||
if (meta && meta.selfdestruct && Env.selfDestructTo) {
|
||||
Env.selfDestructTo[chanName] = setTimeout(function () {
|
||||
expireChannel(Env, chanName);
|
||||
}, TEMPORARY_CHANNEL_LIFETIME);
|
||||
}
|
||||
};
|
||||
|
||||
/* checkExpired
|
||||
@ -478,8 +487,8 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
|
||||
cb(null, lkh);
|
||||
}));
|
||||
}).nThen((w) => {
|
||||
// XXX entire block and getHashOffset to remove?
|
||||
|
||||
// If we're here it means we asked for a lastKnownHash but it is old (not in the index)
|
||||
// and this is not a "chainpad" channel so we can't recover from a checkpoint.
|
||||
|
||||
// skip past this block if the offset is anything other than -1
|
||||
// this basically makes these first two nThen blocks behave like if-else
|
||||
@ -565,6 +574,10 @@ const handleRPC = function (Env, Server, seq, userId, parsed) {
|
||||
if the provided metadata has an expire time then we also create a task to expire it.
|
||||
*/
|
||||
const handleFirstMessage = function (Env, channelName, metadata) {
|
||||
if (metadata.selfdestruct) {
|
||||
// Set the selfdestruct flag to history keeper ID to handle server crash.
|
||||
metadata.selfdestruct = Env.id;
|
||||
}
|
||||
Env.store.writeMetadata(channelName, JSON.stringify(metadata), function (err) {
|
||||
if (err) {
|
||||
// FIXME tell the user that there was a channel error?
|
||||
|
||||
323
lib/http-commands.js
Normal file
@ -0,0 +1,323 @@
|
||||
var Nacl = require("tweetnacl/nacl-fast");
|
||||
var Util = require('./common-util.js');
|
||||
|
||||
var Challenge = require("./storage/challenge.js");
|
||||
// C.read(Env, id, cb)
|
||||
// C.write(Env,id, data, cb)
|
||||
// C.delete(Env, id, cb)
|
||||
|
||||
|
||||
/*
|
||||
The API for command definition consists of two stages:
|
||||
|
||||
Clients first send a command and its associated parameters.
|
||||
The server validates that the command is supported, and that
|
||||
the provided parameters are valid. If it fails validation for any reason,
|
||||
the server responds with an error and the protocol is aborted.
|
||||
|
||||
COMMANDS[COMMAND_NAME] = function (Env, body, cb) {
|
||||
// inspect parameters in the request body
|
||||
if (!body.essential_parameter) {
|
||||
return void cb('NO');
|
||||
}
|
||||
cb();
|
||||
};
|
||||
|
||||
Commands whose parameters are successfully validated
|
||||
have those parameters stored on the disk (or a relational DB in the future).
|
||||
The server then requests that the client sign their well-formulated
|
||||
command along with a server-generated transaction id ('txid': randomized to prevent replays)
|
||||
and a date (so that it can ensure that the client responds within a reasonable window.
|
||||
|
||||
Clients then respond with a txid and a cryptographic signature
|
||||
which matches the parameters of the command. The server loads the command
|
||||
with the corresponding txid, checks that it was signed within a reasonable time window,
|
||||
validates the signature, and attempts to complete the command's execution:
|
||||
|
||||
COMMAND[COMMAND_NAME].complete = function (Env, body, cb) {
|
||||
doAThing(function (err, values) {
|
||||
if (err) {
|
||||
// Log the error and respond that the command was not successful
|
||||
return void cb("SORRY_BUT_IM_NOT_OK");
|
||||
}
|
||||
cb(void 0, {
|
||||
arbitrary: values,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
In this second stage the protocol can be aborted if the client has done something wrong:
|
||||
(ie. if it did not produce a valid signature for the command)
|
||||
or it can can fail because the server was not able to complete the requested task
|
||||
(ie. because of an I/O error or because an error was thrown and caught).
|
||||
|
||||
It is intended that the server will respond with an appropriate error if
|
||||
the request cannot be completed, and it will respond OK if everything completed successfully.
|
||||
|
||||
*/
|
||||
|
||||
var COMMANDS = {};
|
||||
|
||||
// Methods allowing clients to configure Time-based One-Time Passwords for their login-block,
|
||||
// and to authenticate new sessions once a TOTP secret has been associated with their account,
|
||||
const NOAUTH = require("./challenge-commands/base.js");
|
||||
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
|
||||
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK;
|
||||
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
|
||||
|
||||
const TOTP = require("./challenge-commands/totp.js");
|
||||
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
|
||||
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
|
||||
COMMANDS.TOTP_CHECK = TOTP.TOTP_CHECK;
|
||||
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
|
||||
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK;
|
||||
COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK;
|
||||
|
||||
|
||||
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
||||
|
||||
// this function handles the first stage of the protocol
|
||||
// (the server's validation of the client's request and the generation of its challenge)
|
||||
var handleCommand = function (Env, req, res) {
|
||||
var body = req.body;
|
||||
var command = body.command;
|
||||
|
||||
// reject if the command does not have a corresponding function
|
||||
if (typeof(COMMANDS[command]) !== 'function') {
|
||||
Env.Log.error('CHALLENGE_UNSUPPORTED_COMMAND', command);
|
||||
return void res.status(500).json({
|
||||
error: 'invalid command',
|
||||
});
|
||||
}
|
||||
|
||||
var publicKey = body.publicKey;
|
||||
// reject if they did not provide a valid public key
|
||||
if (!publicKey || typeof(publicKey) !== 'string' || publicKey.length !== 44) {
|
||||
Env.Log.error('CHALLENGE_INVALID_KEY', publicKey);
|
||||
return void res.status(500).json({
|
||||
error: 'Invalid key',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
COMMANDS[command](Env, body, function (err) {
|
||||
if (err) {
|
||||
Env.Log.error('CHALLENGE_COMMAND_EXECUTION_ERROR', {
|
||||
body: body,
|
||||
error: Util.serializeError(err),
|
||||
});
|
||||
// errors returned from commands are passed back to the client
|
||||
// as a weak precaution, we try to only send an error's message
|
||||
// if one exists. This makes it less likely that we'll respond with any
|
||||
// sensitive information in a stack trace. Ideally functions should
|
||||
// only return error messages or codes in the form of a string or number,
|
||||
// but mistakes happen.
|
||||
return void res.status(500).json({
|
||||
error: (err && err.message) || err,
|
||||
});
|
||||
}
|
||||
|
||||
var txid = randomToken();
|
||||
var date = new Date().toISOString();
|
||||
|
||||
var copy = Util.clone(body);
|
||||
copy.txid = txid;
|
||||
copy.date = date;
|
||||
|
||||
// Write the command and challenge to disk, because the challenge protocol
|
||||
// is interactive and the subsequent response might be handled by a different http worker
|
||||
// this makes it so we can avoid holding state in memory
|
||||
Challenge.write(Env, txid, JSON.stringify(copy), function (err) {
|
||||
if (err) {
|
||||
Env.Log.error('CHALLENGE_WRITE_ERROR', Util.serializeError(err));
|
||||
return void res.status(500).json({
|
||||
// arbitrary error message, only intended for debugging
|
||||
error: 'Internal server error 6250',
|
||||
});
|
||||
}
|
||||
// respond with challenge parameters
|
||||
return void res.status(200).json({
|
||||
txid: txid,
|
||||
date: date,
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
Env.Log.error("CHALLENGE_COMMAND_THROWN_ERROR", {
|
||||
error: Util.serializeError(err),
|
||||
});
|
||||
return void res.status(500).json({
|
||||
// arbitrary error message, only intended for debugging
|
||||
error: 'Internal server error 7692',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// this function handles the second stage of the protocol
|
||||
// (the client's response to the server's challenge)
|
||||
var handleResponse = function (Env, req, res) {
|
||||
var body = req.body;
|
||||
|
||||
if (Object.keys(body).some(k => !/(sig|txid)/.test(k))) {
|
||||
Env.Log.error("CHALLENGE_RESPONSE_DEBUGGING", body);
|
||||
// we expect the response to only have two keys
|
||||
// if any more are present then the response is malformed
|
||||
return void res.status(500).json({
|
||||
error: 'extraneous parameters',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// transaction ids are issued to the client by the server
|
||||
// they allow it to recall the full details of the challenge
|
||||
// to which the client is responding
|
||||
var txid = body.txid;
|
||||
|
||||
// if no txid is present, then the server can't look up the corresponding challenge
|
||||
// the response is definitely malformed, so reject it.
|
||||
// Additionally, we expect txids to be 32 characters long (24 Uint8s as base64)
|
||||
// reject txids of any other length
|
||||
if (!txid || typeof(txid) !== 'string' || txid.length !== 32) {
|
||||
Env.Log.error('CHALLENGE_RESPONSE_BAD_TXID', body);
|
||||
return void res.status(500).json({
|
||||
error: "Invalid txid",
|
||||
});
|
||||
}
|
||||
|
||||
var sig = body.sig;
|
||||
if (!sig || typeof(sig) !== 'string' || sig.length !== 88) {
|
||||
Env.Log.error("CHALLENGE_RESPONSE_BAD_SIG", body);
|
||||
return void res.status(500).json({
|
||||
error: "Missing signature",
|
||||
});
|
||||
}
|
||||
|
||||
Challenge.read(Env, txid, function (err, text) {
|
||||
if (err) {
|
||||
Env.Log.error("CHALLENGE_READ_ERROR", {
|
||||
txid: txid,
|
||||
error: Util.serializeError(err),
|
||||
});
|
||||
return void res.status(500).json({
|
||||
error: "Unexpected response",
|
||||
});
|
||||
}
|
||||
|
||||
// garbage collection can clean this up later
|
||||
Challenge.delete(Env, txid, function (err) {
|
||||
if (err) {
|
||||
Env.Log.error("CHALLENGE_DELETION_ERROR", {
|
||||
txid: txid,
|
||||
error: Util.serializeError(err),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var json = Util.tryParse(text);
|
||||
|
||||
if (!json) {
|
||||
Env.Log.error("CHALLENGE_PARSE_ERROR", {
|
||||
txid: txid,
|
||||
});
|
||||
return void res.status(500).json({
|
||||
error: "Internal server error 129",
|
||||
});
|
||||
}
|
||||
|
||||
var publicKey = json.publicKey;
|
||||
if (!publicKey || typeof(publicKey) !== 'string') {
|
||||
// This shouldn't happen, as we expect that the server
|
||||
// will have validated the key to an extent before storing the challenge
|
||||
Env.Log.error('CHALLENGE_INVALID_PUBLICKEY', {
|
||||
publicKey: publicKey,
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: "Invalid public key",
|
||||
});
|
||||
}
|
||||
|
||||
var action;
|
||||
try {
|
||||
action = COMMANDS[json.command].complete;
|
||||
} catch (err2) {}
|
||||
|
||||
if (typeof(action) !== 'function') {
|
||||
Env.Log.error("CHALLENGE_RESPONSE_ACTION_NOT_IMPLEMENTED", json.command);
|
||||
return res.status(501).json({
|
||||
error: 'Not implemented',
|
||||
});
|
||||
}
|
||||
|
||||
var u8_toVerify,
|
||||
u8_sig,
|
||||
u8_publicKey;
|
||||
|
||||
try {
|
||||
u8_toVerify = Nacl.util.decodeUTF8(text);
|
||||
u8_sig = Nacl.util.decodeBase64(sig);
|
||||
u8_publicKey = Nacl.util.decodeBase64(publicKey);
|
||||
} catch (err3) {
|
||||
Env.Log.error('CHALLENGE_RESPONSE_DECODING_ERROR', {
|
||||
text: text,
|
||||
sig: sig,
|
||||
publicKey: publicKey,
|
||||
error: Util.serializeError(err3),
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: "decoding error"
|
||||
});
|
||||
}
|
||||
|
||||
// validate the response
|
||||
var success = Nacl.sign.detached.verify(u8_toVerify, u8_sig, u8_publicKey);
|
||||
if (success !== true) {
|
||||
Env.Log.error("CHALLENGE_RESPONSE_SIGNATURE_FAILURE", {
|
||||
publicKey,
|
||||
});
|
||||
return void res.status(500).json({
|
||||
error: 'Failed signature validation',
|
||||
});
|
||||
}
|
||||
|
||||
// execute the command
|
||||
action(Env, json, function (err, content) {
|
||||
if (err) {
|
||||
Env.Log.error("CHALLENGE_RESPONSE_ACTION_ERROR", {
|
||||
error: Util.serializeError(err),
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: 'Execution error',
|
||||
});
|
||||
}
|
||||
res.status(200).json(content);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.handle = function (Env, req, res /*, next */) {
|
||||
var body = req.body;
|
||||
// we expect that the client has posted some JSON data
|
||||
if (!body) {
|
||||
return void res.status(500).json({
|
||||
error: 'invalid request',
|
||||
});
|
||||
}
|
||||
|
||||
// we only expect responses to challenges to have a 'txid' attribute
|
||||
// further validation is performed in handleResponse
|
||||
if (body.txid) {
|
||||
return void handleResponse(Env, req, res);
|
||||
}
|
||||
|
||||
// we only expect initial requests to have a 'command' attribute
|
||||
// further validation is performed in handleCommand
|
||||
if (body.command) {
|
||||
return void handleCommand(Env, req, res);
|
||||
}
|
||||
|
||||
// if a request is neither a command nor a response, then reject it with an error
|
||||
res.status(500).json({
|
||||
error: 'invalid request',
|
||||
});
|
||||
};
|
||||
622
lib/http-worker.js
Normal file
@ -0,0 +1,622 @@
|
||||
const process = require("node:process");
|
||||
const Http = require("node:http");
|
||||
const Default = require("./defaults");
|
||||
const Path = require("node:path");
|
||||
const Fs = require("node:fs");
|
||||
const nThen = require("nthen");
|
||||
const Util = require("./common-util");
|
||||
const Logger = require("./log");
|
||||
const AuthCommands = require("./http-commands");
|
||||
const MFA = require("./storage/mfa");
|
||||
const Sessions = require("./storage/sessions");
|
||||
|
||||
const DEFAULT_QUERY_TIMEOUT = 5000;
|
||||
const PID = process.pid;
|
||||
|
||||
var Env = JSON.parse(process.env.Env);
|
||||
const response = Util.response(function (errLabel, info) {
|
||||
if (!Env.Log) { return; }
|
||||
Env.Log.error(errLabel, info);
|
||||
});
|
||||
|
||||
const guid = () => {
|
||||
return Util.guid(response._pending);
|
||||
};
|
||||
|
||||
const sendMessage = (msg, cb, opt) => {
|
||||
var txid = guid();
|
||||
var timeout = (opt && opt.timeout) || DEFAULT_QUERY_TIMEOUT;
|
||||
var obj = {
|
||||
pid: PID,
|
||||
txid: txid,
|
||||
content: msg,
|
||||
};
|
||||
response.expect(txid, cb, timeout);
|
||||
process.send(obj);
|
||||
};
|
||||
const Log = {};
|
||||
Logger.levels.forEach(level => {
|
||||
Log[level] = function (tag, info) {
|
||||
sendMessage({
|
||||
command: 'LOG',
|
||||
level: level,
|
||||
tag: tag,
|
||||
info: info,
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
return void console.error(new Error(err));
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
Env.Log = Log;
|
||||
Env.incrementBytesWritten = function () {};
|
||||
|
||||
const EVENTS = {};
|
||||
|
||||
EVENTS.ENV_UPDATE = function (data /*, cb */) {
|
||||
try {
|
||||
Env = JSON.parse(data);
|
||||
Env.Log = Log;
|
||||
Env.incrementBytesWritten = function () {};
|
||||
} catch (err) {
|
||||
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
|
||||
}
|
||||
};
|
||||
|
||||
EVENTS.FLUSH_CACHE = function (data) {
|
||||
if (typeof(data) !== 'number') {
|
||||
return Log.error('INVALID_FRESH_KEY', data);
|
||||
}
|
||||
|
||||
Env.FRESH_KEY = data;
|
||||
[ 'configCache', 'broadcastCache', ].forEach(key => {
|
||||
Env[key] = {};
|
||||
});
|
||||
[ 'officeHeadersCache', 'standardHeadersCache', 'apiHeadersCache', ].forEach(key => {
|
||||
Env[key] = undefined;
|
||||
});
|
||||
};
|
||||
|
||||
process.on('message', msg => {
|
||||
if (!(msg && msg.txid)) { return; }
|
||||
if (msg.type === 'REPLY') {
|
||||
var txid = msg.txid;
|
||||
return void response.handle(txid, [msg.error, msg.value]);
|
||||
} else if (msg.type === 'EVENT') {
|
||||
// response to event...
|
||||
// ie. Update Env, flush cache, etc.
|
||||
var ev = EVENTS[msg.command];
|
||||
if (typeof(ev) === 'function') {
|
||||
return void ev(msg.data, () => {});
|
||||
}
|
||||
}
|
||||
//console.error("UNHANDLED_MESSAGE", msg);
|
||||
});
|
||||
|
||||
|
||||
var applyHeaderMap = function (res, map) {
|
||||
for (let header in map) {
|
||||
if (typeof(map[header]) === 'string') { res.setHeader(header, map[header]); }
|
||||
}
|
||||
};
|
||||
|
||||
var EXEMPT = [
|
||||
/^\/common\/onlyoffice\/.*\.html.*/,
|
||||
/^\/(sheet|presentation|doc)\/inner\.html.*/,
|
||||
/^\/unsafeiframe\/inner\.html.*$/,
|
||||
];
|
||||
|
||||
var cacheHeaders = function (Env, key, headers) {
|
||||
if (Env.DEV_MODE) { return; }
|
||||
Env[key] = headers;
|
||||
};
|
||||
|
||||
var getHeaders = function (Env, type) {
|
||||
var key = type + 'HeadersCache';
|
||||
if (Env[key]) { return Env[key]; }
|
||||
|
||||
var headers = Default.httpHeaders(Env);
|
||||
|
||||
var csp;
|
||||
if (type === 'office') {
|
||||
csp = Default.padContentSecurity(Env);
|
||||
} else if (type === 'diagram') {
|
||||
csp = Default.diagramContentSecurity(Env);
|
||||
} else {
|
||||
csp = Default.contentSecurity(Env);
|
||||
}
|
||||
headers['Content-Security-Policy'] = csp;
|
||||
|
||||
if (Env.NO_SANDBOX) { // handles correct configuration for local development
|
||||
// https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs
|
||||
headers["Cross-Origin-Resource-Policy"] = 'cross-origin';
|
||||
headers["Cross-Origin-Embedder-Policy"] = 'require-corp';
|
||||
}
|
||||
|
||||
// Don't set CSP headers on /api/ endpoints
|
||||
// because they aren't necessary and they cause problems
|
||||
// when duplicated by NGINX in production environments
|
||||
if (type === 'api') {
|
||||
cacheHeaders(Env, key, headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
headers["Cross-Origin-Resource-Policy"] = 'cross-origin';
|
||||
cacheHeaders(Env, key, headers);
|
||||
return headers;
|
||||
};
|
||||
|
||||
var setHeaders = function (req, res) {
|
||||
var type;
|
||||
if (EXEMPT.some(regex => regex.test(req.url))) {
|
||||
type = 'office';
|
||||
} else if (/^\/api\/(broadcast|config)/.test(req.url)) {
|
||||
type = 'api';
|
||||
} else if (/^\/components\/drawio\/src\/main\/webapp\/index.html.*$/.test(req.url)) {
|
||||
type = 'diagram';
|
||||
} else {
|
||||
type = 'standard';
|
||||
}
|
||||
|
||||
var h = getHeaders(Env, type);
|
||||
applyHeaderMap(res, h);
|
||||
};
|
||||
|
||||
const Express = require("express");
|
||||
var app = Express();
|
||||
|
||||
(function () {
|
||||
if (!Env.logFeedback) { return; }
|
||||
|
||||
const logFeedback = function (url) {
|
||||
url.replace(/\?(.*?)=/, function (all, fb) {
|
||||
Log.feedback(fb, '');
|
||||
});
|
||||
};
|
||||
|
||||
app.head(/^\/common\/feedback\.html/, function (req, res, next) {
|
||||
logFeedback(req.url);
|
||||
next();
|
||||
});
|
||||
}());
|
||||
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
var proxyTarget = new URL('', 'ws:localhost');
|
||||
proxyTarget.port = Env.websocketPort;
|
||||
|
||||
const wsProxy = createProxyMiddleware({
|
||||
target: proxyTarget.href,
|
||||
ws: true,
|
||||
logLevel: 'error',
|
||||
logProvider: (p) => {
|
||||
p.error = (data) => {
|
||||
if (/ECONNRESET/.test(data)) { return; }
|
||||
Env.Log.error('HTTP_PROXY_MIDDLEWARE', data);
|
||||
};
|
||||
return p;
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/cryptpad_websocket', wsProxy);
|
||||
|
||||
|
||||
app.use('/blob', function (req, res, next) {
|
||||
/* Head requests are used to check the size of a blob.
|
||||
Clients can configure a maximum size to download automatically,
|
||||
and can manually click to download blobs which exceed that limit. */
|
||||
if (req.method === 'HEAD') {
|
||||
Express.static(Path.resolve(Env.paths.blob), {
|
||||
setHeaders: function (res /*, path, stat */) {
|
||||
res.set('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
|
||||
res.set('Access-Control-Allow-Headers', 'Content-Length');
|
||||
res.set('Access-Control-Expose-Headers', 'Content-Length');
|
||||
}
|
||||
})(req, res, next);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Some GET requests concern the whole file,
|
||||
others only target ranges, either:
|
||||
|
||||
1. a two octet prefix which encodes the length of the metadata in octets
|
||||
2. the metadata itself, excluding the two preceding octets
|
||||
*/
|
||||
|
||||
/*
|
||||
// Example code to demonstrate the types of requests which are handled
|
||||
if (req.method === 'GET') {
|
||||
if (!req.headers.range) {
|
||||
// metadata
|
||||
} else {
|
||||
// full request
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
/* These are pre-flight requests, through which the client
|
||||
confirms with the server that it is permitted to make the
|
||||
actual requests which will follow */
|
||||
if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Access-Control-Allow-Origin');
|
||||
res.setHeader('Access-Control-Max-Age', 1728000);
|
||||
res.setHeader('Content-Type', 'application/octet-stream; charset=utf-8');
|
||||
res.setHeader('Content-Length', 0);
|
||||
res.statusCode = 204;
|
||||
return void res.end();
|
||||
}
|
||||
|
||||
setHeaders(req, res);
|
||||
if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); }
|
||||
else { res.setHeader("Cache-Control", "no-cache"); }
|
||||
next();
|
||||
});
|
||||
|
||||
// serve custom app content from the customize directory
|
||||
// useful for testing pages customized with opengraph data
|
||||
app.use(Express.static(Path.resolve('./customize/www')));
|
||||
app.use(Express.static(Path.resolve('./www')));
|
||||
|
||||
var mainPages = Env.mainPages || Default.mainPages();
|
||||
var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
|
||||
app.get(mainPagePattern, Express.static('./customize'));
|
||||
app.get(mainPagePattern, Express.static('./customize.dist'));
|
||||
|
||||
app.use("/blob", Express.static(Path.resolve(Env.paths.blob), {
|
||||
maxAge: Env.DEV_MODE? "0d": "365d"
|
||||
}));
|
||||
app.head("/datastore", Express.static(Env.paths.data, {
|
||||
maxAge: "0d"
|
||||
}));
|
||||
|
||||
app.use('/block/', function (req, res, next) {
|
||||
var parsed = Path.parse(req.url);
|
||||
var name = parsed.name;
|
||||
// block access control only applies to files
|
||||
// identified by base64-encoded public keys
|
||||
// skip everything else, ie. /block/placeholder.txt
|
||||
if (typeof(name) !== 'string' || name.length !== 44) {
|
||||
return void next();
|
||||
}
|
||||
|
||||
var authorization = req.headers.authorization;
|
||||
|
||||
var mfa_params;
|
||||
nThen(function (w) {
|
||||
// First, check whether the block id in question has any MFA settings stored
|
||||
MFA.read(Env, name, w(function (err, content) {
|
||||
// ENOENT means there are no settings configured
|
||||
// it could be a 404 or an existing block without MFA protection
|
||||
// in either case you can abort and fall through
|
||||
// allowing the static webserver to handle either case
|
||||
if (err && err.code === 'ENOENT') {
|
||||
w.abort();
|
||||
return void next();
|
||||
}
|
||||
|
||||
// we're not expecting other errors. the sensible thing is to fail
|
||||
// closed - meaning assume some protection is in place but that
|
||||
// the settings couldn't be loaded for some reason. block access
|
||||
// to the resource, logging for the admin and responding to the client
|
||||
// with a vague error code
|
||||
if (err) {
|
||||
Log.error('GET_BLOCK_METADATA', err);
|
||||
return void res.status(500).json({
|
||||
code: 500,
|
||||
error: "UNEXPECTED_ERROR",
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, some settings were loaded correctly.
|
||||
// We're expecting stringified JSON, so try to parse it.
|
||||
// Log and respond with an error again if this fails.
|
||||
// If it parses successfully then fall through to the next block.
|
||||
try {
|
||||
mfa_params = JSON.parse(content);
|
||||
} catch (err2) {
|
||||
w.abort();
|
||||
Log.error("INVALID_BLOCK_METADATA", err2);
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
error: "UNEXPECTED_ERROR",
|
||||
});
|
||||
}
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// We should only be able to reach this logic
|
||||
// if we successfully loaded and parsed some JSON
|
||||
// representing the user's MFA settings.
|
||||
|
||||
// Failures at this point relate to insufficient or incorrect authorization.
|
||||
// This function standardizes how we reject such requests.
|
||||
|
||||
// So far the only additional factor which is supported is TOTP.
|
||||
// We specify what the method is to allow for future alternatives
|
||||
// and inform the client so they can determine how to respond
|
||||
// "401" means "Unauthorized"
|
||||
var no = function () {
|
||||
w.abort();
|
||||
res.status(401).json({
|
||||
method: mfa_params.method,
|
||||
code: 401
|
||||
});
|
||||
};
|
||||
|
||||
// if you are here it is because this block is protected by MFA.
|
||||
// they will need to provide a JSON Web Token, so we can reject them outright
|
||||
// if one is not present in their authorization header
|
||||
if (!authorization) { return void no(); }
|
||||
|
||||
// The authorization header should be of the form
|
||||
// "Authorization: Bearer <JWT>"
|
||||
// We can reject the request if it is malformed.
|
||||
let token = authorization.replace(/^Bearer\s+/, '').trim();
|
||||
if (!token) { return void no(); }
|
||||
|
||||
Sessions.read(Env, name, token, function (err, contentStr) {
|
||||
if (err) {
|
||||
Log.error('SESSION_READ_ERROR', err);
|
||||
return res.status(401).json({
|
||||
method: mfa_params.method,
|
||||
code: 401,
|
||||
});
|
||||
}
|
||||
|
||||
let content = Util.tryParse(contentStr);
|
||||
|
||||
if (content.mfa && content.mfa.exp && ((+new Date()) > content.mfa.exp)) {
|
||||
Log.error("OTP_SESSION_EXPIRED", content.mfa);
|
||||
Sessions.delete(Env, name, token, function (err) {
|
||||
if (err) {
|
||||
Log.error('SESSION_DELETE_EXPIRED_ERROR', err);
|
||||
return;
|
||||
}
|
||||
Log.info('SESSION_DELETE_EXPIRED', err);
|
||||
});
|
||||
return void no();
|
||||
}
|
||||
|
||||
// we could also check whether the content of the file matches the token,
|
||||
// but clients don't have any influence over the reference and can only
|
||||
// request to create tokens that are scoped to a public key they control.
|
||||
// I don' think there's any practical benefit to such a check.
|
||||
|
||||
// So, interpret the existence of a file in that location as the continued
|
||||
// validity of the session. Fall through and let the built-in webserver
|
||||
// handle the 404 or serving the file.
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO this would be a good place to update a block's atime
|
||||
// in a manner independent of the filesystem. ie. for detecting and archiving
|
||||
// inactive accounts in a way that will not be invalidated by other forms of access
|
||||
// like filesystem backups.
|
||||
app.use("/block", Express.static(Path.resolve(Env.paths.block), {
|
||||
maxAge: "0d",
|
||||
}));
|
||||
|
||||
app.use("/customize", Express.static('customize'));
|
||||
app.use("/customize", Express.static('customize.dist'));
|
||||
app.use("/customize.dist", Express.static('customize.dist'));
|
||||
app.use(/^\/[^\/]*$/, Express.static('customize'));
|
||||
app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
|
||||
|
||||
// if dev mode: never cache
|
||||
var cacheString = function () {
|
||||
return (Env.FRESH_KEY? '-' + Env.FRESH_KEY: '') + (Env.DEV_MODE? '-' + (+new Date()): '');
|
||||
};
|
||||
|
||||
var makeRouteCache = function (template, cacheName) {
|
||||
var cleanUp = {};
|
||||
var cache = Env[cacheName] = Env[cacheName] || {};
|
||||
|
||||
return function (req, res) {
|
||||
var host = req.headers.host.replace(/\:[0-9]+/, '');
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
// don't cache anything if you're in dev mode
|
||||
if (Env.DEV_MODE) {
|
||||
return void res.send(template(host));
|
||||
}
|
||||
// generate a lookup key for the cache
|
||||
var cacheKey = host + ':' + cacheString();
|
||||
|
||||
// FIXME mutable
|
||||
// we must be able to clear the cache when updating any mutable key
|
||||
// if there's nothing cached for that key...
|
||||
if (!cache[cacheKey]) {
|
||||
// generate the response and cache it in memory
|
||||
cache[cacheKey] = template(host);
|
||||
// and create a function to conditionally evict cache entries
|
||||
// which have not been accessed in the last 20 seconds
|
||||
cleanUp[cacheKey] = Util.throttle(function () {
|
||||
delete cleanUp[cacheKey];
|
||||
delete cache[cacheKey];
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
// successive calls to this function
|
||||
cleanUp[cacheKey]();
|
||||
return void res.send(cache[cacheKey]);
|
||||
};
|
||||
};
|
||||
|
||||
var serveConfig = makeRouteCache(function () {
|
||||
return [
|
||||
'define(function(){',
|
||||
'return ' + JSON.stringify({
|
||||
requireConf: {
|
||||
waitSeconds: 600,
|
||||
urlArgs: 'ver=' + Env.version + cacheString(),
|
||||
},
|
||||
removeDonateButton: (Env.removeDonateButton === true),
|
||||
allowSubscriptions: (Env.allowSubscriptions === true),
|
||||
websocketPath: Env.websocketPath,
|
||||
httpUnsafeOrigin: Env.httpUnsafeOrigin,
|
||||
adminEmail: Env.adminEmail,
|
||||
adminKeys: Env.admins,
|
||||
inactiveTime: Env.inactiveTime,
|
||||
supportMailbox: Env.supportMailbox,
|
||||
defaultStorageLimit: Env.defaultStorageLimit,
|
||||
maxUploadSize: Env.maxUploadSize,
|
||||
premiumUploadSize: Env.premiumUploadSize,
|
||||
restrictRegistration: Env.restrictRegistration,
|
||||
httpSafeOrigin: Env.httpSafeOrigin,
|
||||
enableEmbedding: Env.enableEmbedding,
|
||||
fileHost: Env.fileHost,
|
||||
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
||||
listMyInstance: Env.listMyInstance,
|
||||
accounts_api: Env.accounts_api,
|
||||
}, null, '\t'),
|
||||
'});'
|
||||
].join(';\n');
|
||||
}, 'configCache');
|
||||
|
||||
var serveBroadcast = makeRouteCache(function () {
|
||||
var maintenance = Env.maintenance;
|
||||
if (maintenance && maintenance.end && maintenance.end < (+new Date())) {
|
||||
maintenance = undefined;
|
||||
}
|
||||
return [
|
||||
'define(function(){',
|
||||
'return ' + JSON.stringify({
|
||||
lastBroadcastHash: Env.lastBroadcastHash,
|
||||
surveyURL: Env.surveyURL,
|
||||
maintenance: maintenance
|
||||
}, null, '\t'),
|
||||
'});'
|
||||
].join(';\n');
|
||||
}, 'broadcastCache');
|
||||
|
||||
app.get('/api/config', serveConfig);
|
||||
app.get('/api/broadcast', serveBroadcast);
|
||||
|
||||
var Define = function (obj) {
|
||||
return `define(function (){
|
||||
return ${JSON.stringify(obj, null, '\t')};
|
||||
});`;
|
||||
};
|
||||
|
||||
app.get('/api/instance', function (req, res) {
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send(Define({
|
||||
name: Env.instanceName,
|
||||
description: Env.instanceDescription,
|
||||
location: Env.instanceJurisdiction,
|
||||
notice: Env.instanceNotice,
|
||||
}));
|
||||
});
|
||||
|
||||
var four04_path = Path.resolve('./customize.dist/404.html');
|
||||
var fivehundred_path = Path.resolve('./customize.dist/500.html');
|
||||
var custom_four04_path = Path.resolve('./customize/404.html');
|
||||
var custom_fivehundred_path = Path.resolve('./customize/500.html');
|
||||
|
||||
var send404 = function (res, path) {
|
||||
if (!path && path !== four04_path) { path = four04_path; }
|
||||
Fs.exists(path, function (exists) {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
if (exists) { return Fs.createReadStream(path).pipe(res); }
|
||||
send404(res);
|
||||
});
|
||||
};
|
||||
var send500 = function (res, path) {
|
||||
if (!path && path !== fivehundred_path) { path = fivehundred_path; }
|
||||
Fs.exists(path, function (exists) {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
if (exists) { return Fs.createReadStream(path).pipe(res); }
|
||||
send500(res);
|
||||
});
|
||||
};
|
||||
|
||||
app.get('/api/updatequota', function (req, res) {
|
||||
if (!Env.accounts_api) {
|
||||
res.status(404);
|
||||
return void send404(res);
|
||||
}
|
||||
sendMessage({
|
||||
command: 'UPDATE_QUOTA',
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return void send500(res);
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/profiling', function (req, res) {
|
||||
if (!Env.enableProfiling) { return void send404(res); }
|
||||
sendMessage({
|
||||
command: 'GET_PROFILING_DATA',
|
||||
}, (err, value) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return void send500(res);
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send(JSON.stringify({
|
||||
bytesWritten: value,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
// This endpoint handles authenticated RPCs over HTTP
|
||||
// via an interactive challenge-response protocol
|
||||
app.use(Express.json());
|
||||
app.post('/api/auth', function (req, res, next) {
|
||||
AuthCommands.handle(Env, req, res, next);
|
||||
});
|
||||
|
||||
app.use(function (req, res /*, next */) {
|
||||
if (/^(\/favicon\.ico\/|.*\.js\.map)$/.test(req.url)) {
|
||||
// ignore common 404s
|
||||
} else {
|
||||
Log.info('HTTP_404', req.url);
|
||||
}
|
||||
|
||||
res.status(404);
|
||||
send404(res, custom_four04_path);
|
||||
});
|
||||
|
||||
// default message for thrown errors in ExpressJS routes
|
||||
app.use(function (err, req, res /*, next*/) {
|
||||
Log.error('EXPRESSJS_ROUTING', {
|
||||
error: err.stack || err,
|
||||
});
|
||||
res.status(500);
|
||||
send500(res, custom_fivehundred_path);
|
||||
});
|
||||
|
||||
var server = Http.createServer(app);
|
||||
|
||||
nThen(function (w) {
|
||||
server.listen(Env.httpPort, w());
|
||||
if (Env.httpSafePort) {
|
||||
server.listen(Env.httpSafePort, w());
|
||||
}
|
||||
server.on('upgrade', function (req, socket, head) {
|
||||
// TODO warn admins that websockets should only be proxied in this way in a dev environment
|
||||
// in production it's more efficient to have your reverse proxy (NGINX) directly forward
|
||||
// websocket traffic to the correct port (Env.websocketPort)
|
||||
wsProxy.upgrade(req, socket, head);
|
||||
});
|
||||
}).nThen(function () {
|
||||
// TODO inform the parent process that this worker is ready
|
||||
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function (err) {
|
||||
console.error('[%s] UNCAUGHT EXCEPTION IN HTTP WORKER', new Date());
|
||||
console.error(err);
|
||||
console.error("TERMINATING");
|
||||
process.exit(1);
|
||||
});
|
||||
@ -5,7 +5,6 @@ const Core = require("./commands/core");
|
||||
const Admin = require("./commands/admin-rpc");
|
||||
const Pinning = require("./commands/pin-rpc");
|
||||
const Quota = require("./commands/quota");
|
||||
const Block = require("./commands/block");
|
||||
const Metadata = require("./commands/metadata");
|
||||
const Channel = require("./commands/channel");
|
||||
const Upload = require("./commands/upload");
|
||||
@ -54,8 +53,6 @@ const AUTHENTICATED_USER_TARGETED = {
|
||||
UPLOAD_COMPLETE: Upload.complete,
|
||||
UPLOAD_CANCEL: Upload.cancel,
|
||||
OWNED_UPLOAD_COMPLETE: Upload.complete_owned,
|
||||
WRITE_LOGIN_BLOCK: Block.writeLoginBlock,
|
||||
REMOVE_LOGIN_BLOCK: Block.removeLoginBlock,
|
||||
ADMIN: Admin.command,
|
||||
SET_METADATA: Metadata.setMetadata,
|
||||
};
|
||||
|
||||
69
lib/storage/basic.js
Normal file
@ -0,0 +1,69 @@
|
||||
/* Mulfi-factor auth requires some rudimentary storage methods
|
||||
for a number of data types:
|
||||
|
||||
* "challenges" (described in challenge.js)
|
||||
* account settings for MFA (described in mfa.js)
|
||||
* session tokens (described in sessions.js)
|
||||
|
||||
Each data type requires the same three simple methods:
|
||||
|
||||
* read
|
||||
* write
|
||||
* delete
|
||||
|
||||
These could be implemented as tables in a relational database, but committing to a relational DB
|
||||
is a big decision, so these methods are instead implemented using the filesystem, with each
|
||||
file's path and naming convention implemented outside of this module.
|
||||
|
||||
Feel free to migrate all of these to a relational DB at some point in the future if you like.
|
||||
|
||||
*/
|
||||
|
||||
const Basic = module.exports;
|
||||
const Fs = require("node:fs");
|
||||
const Path = require("node:path");
|
||||
|
||||
var pathError = (cb) => {
|
||||
setTimeout(function () {
|
||||
cb(new Error("INVALID_PATH"));
|
||||
});
|
||||
};
|
||||
|
||||
Basic.read = function (Env, path, cb) {
|
||||
if (!path) { return void pathError(cb); }
|
||||
Fs.readFile(path, 'utf8', (err, content) => {
|
||||
if (err) { return void cb(err); }
|
||||
cb(void 0, content);
|
||||
});
|
||||
};
|
||||
Basic.readDir = function (Env, path, cb) {
|
||||
if (!path) { return void pathError(cb); }
|
||||
Fs.readdir(path, cb);
|
||||
};
|
||||
|
||||
Basic.write = function (Env, path, data, cb) {
|
||||
if (!path) { return void pathError(cb); }
|
||||
var dirpath = Path.dirname(path);
|
||||
Fs.mkdir(dirpath, { recursive: true }, function (err) {
|
||||
if (err) { return void cb(err); }
|
||||
// the 'wx' flag causes writes to fail with EEXIST if a file is already present at the given path
|
||||
// this could be overridden with options in the future if necessary, but it seems like a sensible default
|
||||
Fs.writeFile(path, data, { flag: 'wx', }, cb);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO I didn't bother implementing the usual "archive/restore/delete-from-archives" methods
|
||||
// because they didn't seem particularly important for the data implemented with this module.
|
||||
// They're still worth considering, though, so don't let my ommission stop you.
|
||||
// Login blocks could probably be implemented with this module if these methods were supported.
|
||||
// --Aaron
|
||||
Basic.delete = function (Env, path, cb) {
|
||||
if (!path) { return void pathError(cb); }
|
||||
Fs.rm(path, cb);
|
||||
};
|
||||
Basic.deleteDir = function (Env, path, cb) {
|
||||
if (!path) { return void pathError(cb); }
|
||||
Fs.rm(path, { recursive: true, force: true }, cb);
|
||||
};
|
||||
|
||||
|
||||
@ -116,6 +116,8 @@ Block.check = function (Env, publicKey, _cb) { // 'check' because 'exists' impli
|
||||
Fs.access(path, Fs.constants.F_OK, cb);
|
||||
};
|
||||
|
||||
Block.MAX_SIZE = 256;
|
||||
|
||||
Block.write = function (Env, publicKey, buffer, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
var path = Block.mkPath(Env, publicKey);
|
||||
|
||||
39
lib/storage/challenge.js
Normal file
@ -0,0 +1,39 @@
|
||||
const Basic = require("./basic.js");
|
||||
const Path = require("node:path");
|
||||
|
||||
const Challenge = module.exports;
|
||||
/* This module manages storage used to implement a public-key authenticated
|
||||
challenge-response protocol.
|
||||
|
||||
Each 'challenge' is only intended to be valid for a short period of time.
|
||||
1. A client makes a request of the server
|
||||
2. The server stores their request with a nonce and challenges them to sign for this request
|
||||
3. If the client successfully signs for the request within a short window then the request is executed
|
||||
4. Whether the signature is valid or not, the challenge is removed
|
||||
|
||||
Thus, we only expect challenges to remain in storage if the request was aborted or interrupted
|
||||
for some unexpected reason.
|
||||
|
||||
Some form of garbage collection should be implemented in the future.
|
||||
*/
|
||||
|
||||
const pathFromId = function (Env, id) {
|
||||
if (!id || typeof(id) !== 'string') { return void console.error('CHALLENGE_BAD_ID', id); }
|
||||
return Path.join(Env.paths.base, "challenges", id.slice(0, 2), id);
|
||||
};
|
||||
|
||||
Challenge.read = function (Env, id, cb) {
|
||||
var path = pathFromId(Env, id);
|
||||
Basic.read(Env, path, cb);
|
||||
};
|
||||
|
||||
Challenge.write = function (Env, id, data, cb) {
|
||||
var path = pathFromId(Env, id);
|
||||
Basic.write(Env, path, data, cb);
|
||||
};
|
||||
|
||||
Challenge.delete = function (Env, id, cb) {
|
||||
var path = pathFromId(Env, id);
|
||||
Basic.delete(Env, path, cb);
|
||||
};
|
||||
|
||||
@ -623,7 +623,7 @@ var listChannels = function (root, handler, cb, fast) {
|
||||
var metadataName;
|
||||
|
||||
// if the current file is not the channel data, then it must be metadata
|
||||
if (!/^[0-9a-fA-F]{32, 33}\.ndjson$/.test(item)) {
|
||||
if (!/^[0-9a-fA-F]{32,33}\.ndjson$/.test(item)) {
|
||||
metadataName = item;
|
||||
|
||||
channelName = item.replace(/\.metadata/, '');
|
||||
|
||||
88
lib/storage/mfa.js
Normal file
@ -0,0 +1,88 @@
|
||||
const Basic = require("./basic");
|
||||
const Path = require("node:path");
|
||||
const Util = require("../common-util");
|
||||
const Sessions = require("./sessions");
|
||||
const nThen = require("nthen");
|
||||
|
||||
const MFA = module.exports;
|
||||
|
||||
/*
|
||||
This module manages storage related to accounts' multi-factor authentication settings.
|
||||
|
||||
These settings are checked every time a block is accessed, so we do as little as possible
|
||||
so that it can be accessed quickly.
|
||||
|
||||
*/
|
||||
|
||||
/* The path for a given account's settings is based on the public signing key
|
||||
which identifies its "login block". We expect that any action to create or access
|
||||
a block will be authenticated with a challenge-response protocol, so we
|
||||
don't bother checking the validity of an identifier in here aside from
|
||||
ensuring that it won't throw when using string methods.
|
||||
|
||||
*/
|
||||
var pathFromId = function (Env, id) {
|
||||
if (!id || typeof(id) !== 'string') { return; }
|
||||
id = Util.escapeKeyCharacters(id);
|
||||
return Path.join(Env.paths.base, "mfa", id.slice(0, 2), `${id}.json`);
|
||||
};
|
||||
|
||||
MFA.read = function (Env, id, cb) {
|
||||
var path = pathFromId(Env, id);
|
||||
Basic.read(Env, path, cb);
|
||||
};
|
||||
|
||||
// data should be a string
|
||||
MFA.write = function (Env, id, data, cb) {
|
||||
var path = pathFromId(Env, id);
|
||||
Basic.write(Env, path, data, cb);
|
||||
};
|
||||
|
||||
MFA.delete = function (Env, id, cb) {
|
||||
var path = pathFromId(Env, id);
|
||||
Basic.delete(Env, path, cb);
|
||||
};
|
||||
|
||||
MFA.revoke = function (Env, publicKey, cb) {
|
||||
nThen(function (w) {
|
||||
MFA.delete(Env, publicKey, w(function (err) {
|
||||
if (!err) { return; }
|
||||
w.abort();
|
||||
Env.Log.error('TOTP_REVOKE_MFA_DELETE', {
|
||||
error: err,
|
||||
publicKey: publicKey,
|
||||
});
|
||||
cb('MFA_ERROR');
|
||||
}));
|
||||
}).nThen(function () {
|
||||
Sessions.deleteUser(Env, publicKey, function (err) {
|
||||
if (!err) { return; }
|
||||
// If we can't delete the sessions, don't send an error, just log to the server.
|
||||
// The MFA will still be correctly disabled as long as the first step is done.
|
||||
Env.Log.error('TOTP_REVOKE_SESSIONS__DELETE', {
|
||||
error: err,
|
||||
publicKey: publicKey,
|
||||
});
|
||||
});
|
||||
}).nThen(function () {
|
||||
cb(void 0, {
|
||||
success: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
MFA.copy = function (Env, oldKey, newKey, cb) {
|
||||
let content;
|
||||
nThen(function (w) {
|
||||
MFA.read(Env, oldKey, w(function (err, c) {
|
||||
if (err) {
|
||||
// No MFA configured, nothing to copy
|
||||
w.abort();
|
||||
return void cb();
|
||||
}
|
||||
content = c;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
MFA.write(Env, newKey, content, cb);
|
||||
});
|
||||
};
|
||||
58
lib/storage/sessions.js
Normal file
@ -0,0 +1,58 @@
|
||||
const Basic = require("./basic");
|
||||
const Path = require("node:path");
|
||||
const Nacl = require("tweetnacl/nacl-fast");
|
||||
const Util = require("../common-util");
|
||||
|
||||
const Sessions = module.exports;
|
||||
/* This module manages storage for per-acccount session tokens - currently assumed to be
|
||||
JSON Web Tokens (JWTs).
|
||||
|
||||
Decisions about what goes into each of those JWTs happens upstream, so the storage
|
||||
itself is relatively unopinionated.
|
||||
|
||||
The key things to understand are:
|
||||
|
||||
* valid sessions allow the holder of a given JWT to access a given "login block"
|
||||
* JWTs are signed with a key held in the server's memory. If that key leaks then it should be rotated (with the SET_BEARER_SECRET decree) to invalidate all existing JWTs. Under these conditions then all tokens signed with the old key can be removed. Garbage collection of these older tokens is not implemented.
|
||||
* it is expected that any given login-block can have multiple active sessions (for different devices, or if their browser clears its cache automatically). All sessions for a given block are stored in a per-user directory which is intended to make listing or iterating over them simple.
|
||||
* It could be desirable to expose the list of sessions to the relevant user and allow them to revoke sessions individually or en-masse, though this is not currently implemented.
|
||||
|
||||
*/
|
||||
|
||||
var pathFromId = function (Env, id, ref) {
|
||||
if (!id || typeof(id) !== 'string') { return; }
|
||||
id = Util.escapeKeyCharacters(id);
|
||||
return Path.join(Env.paths.base, "sessions", id.slice(0, 2), id, ref);
|
||||
};
|
||||
|
||||
Sessions.randomId = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
||||
|
||||
Sessions.read = function (Env, id, ref, cb) {
|
||||
var path = pathFromId(Env, id, ref);
|
||||
Basic.read(Env, path, cb);
|
||||
};
|
||||
|
||||
Sessions.write = function (Env, id, ref, data, cb) {
|
||||
var path = pathFromId(Env, id, ref);
|
||||
Basic.write(Env, path, data, cb);
|
||||
};
|
||||
|
||||
Sessions.delete = function (Env, id, ref, cb) {
|
||||
var path = pathFromId(Env, id, ref);
|
||||
Basic.delete(Env, path, cb);
|
||||
};
|
||||
|
||||
Sessions.deleteUser = function (Env, id, cb) {
|
||||
if (!id || typeof(id) !== 'string') { return; }
|
||||
id = Util.escapeKeyCharacters(id);
|
||||
var dirPath = Path.join(Env.paths.base, "sessions", id.slice(0, 2), id);
|
||||
|
||||
Basic.readDir(Env, dirPath, (err, files) => {
|
||||
var checkContent = !files || (Array.isArray(files) && files.every((file) => {
|
||||
return file && file.length === 32;
|
||||
}));
|
||||
if (!checkContent) { return void cb('INVALID_SESSIONS_DIR'); }
|
||||
Basic.deleteDir(Env, dirPath, cb);
|
||||
});
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ const OS = require("os");
|
||||
const { fork } = require('child_process');
|
||||
const Workers = module.exports;
|
||||
const PID = process.pid;
|
||||
const Block = require("../storage/block");
|
||||
|
||||
const DB_PATH = 'lib/workers/db-worker';
|
||||
const MAX_JOBS = 16;
|
||||
@ -260,22 +261,7 @@ Workers.initialize = function (Env, config, _cb) {
|
||||
};
|
||||
|
||||
nThen(function (w) {
|
||||
const max = config.maxWorkers;
|
||||
|
||||
var limit;
|
||||
if (typeof(max) !== 'undefined') {
|
||||
// the admin provided a limit on the number of workers
|
||||
if (typeof(max) === 'number' && !isNaN(max)) {
|
||||
if (max < 1) {
|
||||
Log.info("INSUFFICIENT_MAX_WORKERS", max);
|
||||
limit = 1;
|
||||
}
|
||||
limit = max;
|
||||
} else {
|
||||
Log.error("INVALID_MAX_WORKERS", '[' + max + ']');
|
||||
}
|
||||
}
|
||||
|
||||
var limit = Env.maxWorkers;
|
||||
var logged;
|
||||
|
||||
OS.cpus().forEach(function (cpu, index) {
|
||||
@ -472,6 +458,16 @@ Workers.initialize = function (Env, config, _cb) {
|
||||
};
|
||||
|
||||
Env.validateLoginBlock = function (publicKey, signature, block, cb) {
|
||||
if (!block || !block.length || block.length > Block.MAX_SIZE) {
|
||||
return void setTimeout(function () {
|
||||
Env.Log.error('E_INVALID_BLOCK_SIZE', {
|
||||
size: block.length,
|
||||
});
|
||||
|
||||
cb('E_INVALID_BLOCK_SIZE');
|
||||
});
|
||||
}
|
||||
|
||||
sendCommand({
|
||||
command: 'VALIDATE_LOGIN_BLOCK',
|
||||
publicKey: publicKey,
|
||||
|
||||
1647
package-lock.json
generated
46
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cryptpad",
|
||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.0",
|
||||
"license": "AGPL-3.0+",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -18,16 +18,50 @@
|
||||
"express": "~4.18.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"get-folder-size": "^2.0.1",
|
||||
"netflux-websocket": "^0.1.20",
|
||||
"netflux-websocket": "^1.0.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"notp": "^2.0.3",
|
||||
"nthen": "0.1.8",
|
||||
"prompt-confirm": "^2.0.4",
|
||||
"pull-stream": "^3.6.1",
|
||||
"saferphore": "0.0.1",
|
||||
"sortify": "^1.0.4",
|
||||
"stream-to-pull-stream": "^1.7.2",
|
||||
"thirty-two": "^1.0.2",
|
||||
"tweetnacl": "~0.12.2",
|
||||
"ulimit": "0.0.2",
|
||||
"ws": "^3.3.1"
|
||||
"ws": "^3.3.1",
|
||||
|
||||
"alertify.js": "1.0.11",
|
||||
"bootstrap": "^4.0.0",
|
||||
"bootstrap-tokenfield": "^0.12.0",
|
||||
"chainpad": "^5.2.6",
|
||||
"chainpad-listmap": "^1.0.0",
|
||||
"chainpad-netflux": "^1.0.0",
|
||||
"ckeditor": "npm:ckeditor4@^4.22.1",
|
||||
"codemirror": "^5.19.0",
|
||||
"components-font-awesome": "^4.6.3",
|
||||
"croppie": "^2.5.0",
|
||||
"file-saver": "1.3.1",
|
||||
"hyper-json": "~1.4.0",
|
||||
"jquery": "3.6.0",
|
||||
"json.sortify": "~2.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"dragula": "3.7.2",
|
||||
"html2canvas": "^1.4.0",
|
||||
"localforage": "^1.5.2",
|
||||
"marked": "^4.3.0",
|
||||
"mathjax": "3.0.5",
|
||||
"open-sans-fontface": "^1.4.0",
|
||||
"require-css": "0.1.10",
|
||||
"requirejs": "2.3.5",
|
||||
"requirejs-plugins": "^1.0.2",
|
||||
"scrypt-async": "1.2.0",
|
||||
"sortablejs": "^1.6.0",
|
||||
"drawio": "cryptpad/drawio-npm#npm",
|
||||
"pako": "^2.1.0",
|
||||
"x2js": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jshint": "^2.13.4",
|
||||
@ -35,9 +69,13 @@
|
||||
},
|
||||
"overrides": {
|
||||
"glob-parent": "5.1.2",
|
||||
"set-value": "4.0.1"
|
||||
"set-value": "4.0.1",
|
||||
"minimist": "~1.2.3",
|
||||
"minimatch": "~3.1.2",
|
||||
"jquery": "3.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"install:components": "node scripts/copy-components.js",
|
||||
"start": "node server.js",
|
||||
"dev": "DEV=1 node server.js",
|
||||
"fresh": "FRESH=1 node server.js",
|
||||
|
||||
@ -22,7 +22,9 @@ The most recent version and all past release notes can be found [here](https://g
|
||||
|
||||
## Setup using Docker
|
||||
|
||||
See [CryptPad-Docker](https://github.com/cryptpad/cryptpad-docker) repository for details on how to get up-and-running with CryptPad in Docker. This repository is maintained by the community and not officially supported.
|
||||
You can find `Dockerfile`, `docker-compose.yml` and `docker-entrypoint.sh` files at the root of this repository. We also publish every release on [Docker Hub](https://hub.docker.com/r/cryptpad/cryptpad) as AMD64 & ARM64 official images.
|
||||
|
||||
Previously, Docker images were community maintained, had their own repository and weren't official supported. We changed that with v5.4.0 during July 2023. Thanks to @promasu for all the work on the community images.
|
||||
|
||||
# Security
|
||||
|
||||
|
||||