mirror of
https://github.com/freedit-org/freedit.git
synced 2026-01-10 13:08:13 -05:00
[wip] e2ee messqge
This commit is contained in:
@@ -9,7 +9,8 @@ use crate::{
|
||||
mod_inn_post, post, post_delete, post_downvote, post_hide, post_lock, post_pin,
|
||||
post_upvote, preview, tag,
|
||||
},
|
||||
meta_handler::{handler_404, home, robots, style},
|
||||
message::{key, key_post, message},
|
||||
meta_handler::{encoding_js, encryption_js, handler_404, home, robots, style},
|
||||
notification::notification,
|
||||
solo::{solo, solo_delete, solo_like, solo_list, solo_post},
|
||||
tantivy::search,
|
||||
@@ -104,11 +105,15 @@ pub async fn router() -> Router {
|
||||
.route("/feed/star/:item_id", get(feed_star))
|
||||
.route("/feed/subscribe/:uid/:item_id", get(feed_subscribe))
|
||||
.route("/feed/read/:item_id", get(feed_read))
|
||||
.route("/search", get(search));
|
||||
.route("/search", get(search))
|
||||
.route("/message/:uid", get(message))
|
||||
.route("/key", get(key).post(key_post));
|
||||
|
||||
let router_static = Router::new()
|
||||
.route("/static/style.css", get(style))
|
||||
.route("/robots.txt", get(robots))
|
||||
.route("/static/js/encryption-helper.js", get(encryption_js))
|
||||
.route("/static/js/encoding-helper.js", get(encoding_js))
|
||||
.nest_service("/static/avatars", ServeDir::new(&CONFIG.avatars_path))
|
||||
.nest_service("/static/inn_icons", ServeDir::new(&CONFIG.inn_icons_path))
|
||||
.nest_service("/static/upload", ServeDir::new(&CONFIG.upload_path));
|
||||
|
||||
@@ -258,10 +258,10 @@ pub(crate) async fn admin_view(
|
||||
let one_fmt = unescape(&format!("{:?}", one)).unwrap();
|
||||
ones.push(format!("{key}: {one_fmt}"));
|
||||
}
|
||||
"feed_errs" => {
|
||||
let feed_id = ivec_to_u32(&k);
|
||||
let err = String::from_utf8_lossy(&v);
|
||||
ones.push(format!("{feed_id}: {err}"));
|
||||
"feed_errs" | "pub_keys" => {
|
||||
let id = ivec_to_u32(&k);
|
||||
let msg = String::from_utf8_lossy(&v);
|
||||
ones.push(format!("{id}: {msg}"));
|
||||
}
|
||||
"drafts" => {
|
||||
let uid = u8_slice_to_u32(&k[0..4]);
|
||||
|
||||
99
src/controller/message.rs
Normal file
99
src/controller/message.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
headers::Cookie,
|
||||
response::{IntoResponse, Redirect},
|
||||
Form, TypedHeader,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{controller::fmt::clean_html, error::AppError, DB};
|
||||
|
||||
use super::{
|
||||
db_utils::u32_to_ivec,
|
||||
meta_handler::{into_response, PageData},
|
||||
Claim, SiteConfig,
|
||||
};
|
||||
|
||||
/// Page data: `message.html`
|
||||
#[derive(Template)]
|
||||
#[template(path = "message.html", escape = "none")]
|
||||
struct PageMessage<'a> {
|
||||
page_data: PageData<'a>,
|
||||
pub_key: String,
|
||||
}
|
||||
|
||||
/// `GET /message/:uid`
|
||||
pub(crate) async fn message(
|
||||
cookie: Option<TypedHeader<Cookie>>,
|
||||
Path(uid): Path<u32>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let cookie = cookie.ok_or(AppError::NonLogin)?;
|
||||
let site_config = SiteConfig::get(&DB)?;
|
||||
let claim = Claim::get(&DB, &cookie, &site_config).ok_or(AppError::NonLogin)?;
|
||||
let Some(pub_key) = DB.get(u32_to_ivec(uid))? else {
|
||||
return Err(AppError::Custom(
|
||||
"User has not generated key pairs".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let page_message = PageMessage {
|
||||
page_data: PageData::new("Message", &site_config, Some(claim), false),
|
||||
pub_key: String::from_utf8_lossy(&pub_key).to_string(),
|
||||
};
|
||||
|
||||
Ok(into_response(&page_message))
|
||||
}
|
||||
|
||||
/// Page data: `key.html`
|
||||
#[derive(Template)]
|
||||
#[template(path = "key.html", escape = "none")]
|
||||
struct PageKey<'a> {
|
||||
page_data: PageData<'a>,
|
||||
pub_key: String,
|
||||
}
|
||||
|
||||
/// `GET /key`
|
||||
pub(crate) async fn key(
|
||||
cookie: Option<TypedHeader<Cookie>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let cookie = cookie.ok_or(AppError::NonLogin)?;
|
||||
let site_config = SiteConfig::get(&DB)?;
|
||||
let claim = Claim::get(&DB, &cookie, &site_config).ok_or(AppError::NonLogin)?;
|
||||
|
||||
let pub_key = DB
|
||||
.open_tree("pub_keys")?
|
||||
.get(u32_to_ivec(claim.uid))?
|
||||
.map(|r| String::from_utf8_lossy(&r).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let page_key = PageKey {
|
||||
page_data: PageData::new("Generate Key Pairs", &site_config, Some(claim), false),
|
||||
pub_key,
|
||||
};
|
||||
|
||||
Ok(into_response(&page_key))
|
||||
}
|
||||
|
||||
/// Form data: `/key`
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct FormKey {
|
||||
pub_key: String,
|
||||
}
|
||||
|
||||
/// `POST /key`
|
||||
pub(crate) async fn key_post(
|
||||
cookie: Option<TypedHeader<Cookie>>,
|
||||
Form(input): Form<FormKey>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let cookie = cookie.ok_or(AppError::NonLogin)?;
|
||||
let site_config = SiteConfig::get(&DB)?;
|
||||
let claim = Claim::get(&DB, &cookie, &site_config).ok_or(AppError::NonLogin)?;
|
||||
|
||||
let pub_key = clean_html(&input.pub_key);
|
||||
|
||||
DB.open_tree("pub_keys")?
|
||||
.insert(u32_to_ivec(claim.uid), pub_key.as_str())?;
|
||||
|
||||
Ok(Redirect::to("/key"))
|
||||
}
|
||||
@@ -155,6 +155,14 @@ pub(crate) async fn style() -> (HeaderMap, &'static str) {
|
||||
(headers, &CSS)
|
||||
}
|
||||
|
||||
pub(crate) async fn encryption_js() -> &'static str {
|
||||
include_str!("../../static/js/encryption-helper.js")
|
||||
}
|
||||
|
||||
pub(crate) async fn encoding_js() -> &'static str {
|
||||
include_str!("../../static/js/encoding-helper.js")
|
||||
}
|
||||
|
||||
pub(crate) async fn robots() -> &'static str {
|
||||
include_str!("../../static/robots.txt")
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ pub mod tantivy;
|
||||
|
||||
pub(super) mod admin;
|
||||
pub(super) mod inn;
|
||||
pub(super) mod message;
|
||||
pub(super) mod solo;
|
||||
pub(super) mod upload;
|
||||
pub(super) mod user;
|
||||
|
||||
57
static/js/encoding-helper.js
Normal file
57
static/js/encoding-helper.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// JavaScript is unminified and unchanged, copied from https://github.com/galehouse5/rsa-webcrypto-tool
|
||||
//
|
||||
// The Unlicense: <http://unlicense.org>
|
||||
|
||||
var pemToBase64String = function (value, label) {
|
||||
var lines = value.split("\n");
|
||||
var base64String = "";
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith("-----")) continue;
|
||||
base64String += lines[i];
|
||||
}
|
||||
|
||||
return base64String;
|
||||
};
|
||||
|
||||
var base64StringToArrayBuffer = function (value) {
|
||||
var byteString = atob(value);
|
||||
var byteArray = new Uint8Array(byteString.length);
|
||||
|
||||
for (var i = 0; i < byteString.length; i++) {
|
||||
byteArray[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return byteArray.buffer;
|
||||
};
|
||||
|
||||
var base64StringToPem = function (value, label) {
|
||||
var pem = "-----BEGIN {0}-----\n".replace("{0}", label);
|
||||
|
||||
for (var i = 0; i < value.length; i += 64) {
|
||||
pem += value.substr(i, 64) + "\n";
|
||||
}
|
||||
|
||||
pem += "-----END {0}-----\n".replace("{0}", label);
|
||||
|
||||
return pem;
|
||||
};
|
||||
|
||||
var arrayBufferToBase64String = function (value) {
|
||||
var byteArray = new Uint8Array(value);
|
||||
var byteString = "";
|
||||
|
||||
for (var i = 0; i < byteArray.byteLength; i++) {
|
||||
byteString += String.fromCharCode(byteArray[i]);
|
||||
}
|
||||
|
||||
return btoa(byteString);
|
||||
};
|
||||
|
||||
var pemToArrayBuffer = function (value) {
|
||||
return base64StringToArrayBuffer(pemToBase64String(value));
|
||||
};
|
||||
|
||||
var arrayBufferToPem = function (value, label) {
|
||||
return base64StringToPem(arrayBufferToBase64String(value), label);
|
||||
};
|
||||
85
static/js/encryption-helper.js
Normal file
85
static/js/encryption-helper.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// JavaScript is unminified and unchanged, copied from https://github.com/galehouse5/rsa-webcrypto-tool
|
||||
//
|
||||
// The Unlicense: <http://unlicense.org>
|
||||
|
||||
var rsaAlgorithm = {
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: { name: "SHA-256" }
|
||||
};
|
||||
|
||||
var aesAlgorithm = {
|
||||
name: "AES-GCM",
|
||||
length: 256
|
||||
};
|
||||
|
||||
var aesIVLength = 12;
|
||||
|
||||
var generateRsaKeys = function () {
|
||||
return crypto.subtle.generateKey(rsaAlgorithm,
|
||||
/* extractable: */ true, /* keyUsages: */ ["wrapKey", "unwrapKey"])
|
||||
.catch(function (error) { throw "Error generating keys."; })
|
||||
.then(function (rsaKey) {
|
||||
var exportPublicKey = crypto.subtle.exportKey("spki", rsaKey.publicKey)
|
||||
.catch(function (error) { throw "Error exporting public key."; });
|
||||
var exportPrivateKey = crypto.subtle.exportKey("pkcs8", rsaKey.privateKey)
|
||||
.catch(function (error) { throw "Error exporting private key."; });
|
||||
|
||||
return Promise.all([exportPublicKey, exportPrivateKey])
|
||||
.then(function (keys) { return { publicKeyBuffer: keys[0], privateKeyBuffer: keys[1] }; });
|
||||
});
|
||||
};
|
||||
|
||||
var rsaEncrypt = function (data, rsaPublicKeyBuffer) {
|
||||
var importRsaPublicKey = crypto.subtle.importKey("spki", rsaPublicKeyBuffer, rsaAlgorithm,
|
||||
/* extractable: */ false, /* keyUsages: */ ["wrapKey"])
|
||||
.catch(function (error) { throw "Error importing public key."; });
|
||||
var generateAesKey = crypto.subtle.generateKey(aesAlgorithm,
|
||||
/* extractable: */ true, /* keyUsages: */ ["encrypt"])
|
||||
.catch(function (error) { throw "Error generating symmetric key."; });
|
||||
|
||||
return Promise.all([importRsaPublicKey, generateAesKey])
|
||||
.then(function (keys) {
|
||||
var rsaPublicKey = keys[0], aesKey = keys[1];
|
||||
var aesIV = crypto.getRandomValues(new Uint8Array(aesIVLength));
|
||||
var initializedAesAlgorithm = Object.assign({ iv: aesIV }, aesAlgorithm);
|
||||
|
||||
var wrapAesKey = crypto.subtle.wrapKey("raw", aesKey, rsaPublicKey, rsaAlgorithm)
|
||||
.catch(function (error) { throw "Error encrypting symmetric key."; });
|
||||
var encryptData = crypto.subtle.encrypt(initializedAesAlgorithm, aesKey, data)
|
||||
.catch(function (error) { throw "Error encrypting data."; });
|
||||
|
||||
return Promise.all([wrapAesKey, encryptData])
|
||||
.then(function (buffers) {
|
||||
var wrappedAesKey = new Uint8Array(buffers[0]), encryptedData = new Uint8Array(buffers[1]);
|
||||
var encryptionState = new Uint8Array(wrappedAesKey.length + aesIV.length + encryptedData.length);
|
||||
encryptionState.set(wrappedAesKey, 0);
|
||||
encryptionState.set(aesIV, wrappedAesKey.length);
|
||||
encryptionState.set(encryptedData, wrappedAesKey.length + aesIV.length);
|
||||
return encryptionState.buffer;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var rsaDecrypt = function (data, rsaPrivateKeyBuffer) {
|
||||
return crypto.subtle.importKey("pkcs8", rsaPrivateKeyBuffer, rsaAlgorithm,
|
||||
/* extractable: */ false, /* keyUsages: */ ["unwrapKey"])
|
||||
.catch(function (error) { throw "Error importing private key."; })
|
||||
.then(function (rsaKey) {
|
||||
var wrappedAesKeyLength = rsaAlgorithm.modulusLength / 8;
|
||||
var wrappedAesKey = new Uint8Array(data.slice(0, wrappedAesKeyLength));
|
||||
var aesIV = new Uint8Array(data.slice(wrappedAesKeyLength, wrappedAesKeyLength + aesIVLength));
|
||||
var initializedaesAlgorithm = Object.assign({ iv: aesIV }, aesAlgorithm);
|
||||
|
||||
return crypto.subtle.unwrapKey("raw", wrappedAesKey, rsaKey, rsaAlgorithm, initializedaesAlgorithm,
|
||||
/* extractable: */ false, /* keyUsages: */ ["decrypt"])
|
||||
.catch(function (error) { throw "Error decrypting symmetric key." })
|
||||
.then (function (aesKey) {
|
||||
var encryptedData = new Uint8Array(data.slice(wrappedAesKeyLength + aesIVLength));
|
||||
|
||||
return crypto.subtle.decrypt(initializedaesAlgorithm, aesKey, encryptedData)
|
||||
.catch(function (error) { throw "Error decrypting data." });
|
||||
});
|
||||
});
|
||||
};
|
||||
117
templates/key.html
Normal file
117
templates/key.html
Normal file
@@ -0,0 +1,117 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block csp %}
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self';
|
||||
img-src 'self';script-src 'self' 'unsafe-inline'; style-src 'self'; object-src 'none';
|
||||
font-src 'none'; form-action 'self'; frame-src 'none'; media-src 'none'; manifest-src 'none'; worker-src 'none';">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="box">
|
||||
<div class="content">
|
||||
<p class="title">Generate RSA Keys</p>
|
||||
<p>Your keys and data are kept confidential by running cryptography operations in your browser using
|
||||
<a href="https://www.w3.org/TR/WebCryptoAPI/" target="Web Crypto API">Web Crypto API</a> and
|
||||
JavaScript is left unminified so you can verify page source.
|
||||
</p>
|
||||
<p>The code is copied from: <a href="https://github.com/galehouse5/rsa-webcrypto-tool">https://github.com/galehouse5/rsa-webcrypto-tool</a></p>
|
||||
<p class="has-text-danger">You must keep Private Key yourself and upload public key.</p>
|
||||
<p>You can generate key pair from <a href="https://github.com/galehouse5/rsa-webcrypto-tool">rsa-webcrypto-tool</a> and upload public key here.</p>
|
||||
<button id="button" class="button is-link">Generate Keys</button>
|
||||
<div id="message"></div>
|
||||
|
||||
<div class="box">
|
||||
<fieldset>
|
||||
<div class="field">
|
||||
<div class="is-normal">
|
||||
<label class="label" for="private-key">RSA Private Key</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<p class="help">Download <a id="private-key-download" class="download" download="id_rsa">private key</a></p>
|
||||
<textarea id="private-key-text" name="pri_key" rows="10" class="textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<form id="result" class="box" action="/key" method="post">
|
||||
<fieldset>
|
||||
<div class="field">
|
||||
<div class="is-normal">
|
||||
<label class="label" for="public-key">RSA Public Key</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<textarea id="public-key-text" name="pub_key" rows="10" class="textarea" placeholder="{{pub_key}}">{{pub_key}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label"></div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<p class="help">Only the last uploaded public key will be used.</p>
|
||||
<button type="submit" form="result" class="button is-link">Upload Public Key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra %}
|
||||
<script src="static/js/encoding-helper.js"></script>
|
||||
<script src="static/js/encryption-helper.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var publicKeyText = document.getElementById("public-key-text");
|
||||
var privateKeyText = document.getElementById("private-key-text");
|
||||
var privateKeyDownload = document.getElementById("private-key-download");
|
||||
var button = document.getElementById("button");
|
||||
var message = document.getElementById("message");
|
||||
var result = document.getElementById("result");
|
||||
|
||||
var success = function (keys) {
|
||||
publicKeyText.value = arrayBufferToPem(keys.publicKeyBuffer, "RSA PUBLIC KEY");
|
||||
privateKeyText.value = arrayBufferToPem(keys.privateKeyBuffer, "RSA PRIVATE KEY");
|
||||
privateKeyDownload.href = window.URL.createObjectURL(
|
||||
new Blob([privateKeyText.value], { type: "application/octet-stream" }));
|
||||
result.style.display = "block";
|
||||
message.innerText = null;
|
||||
button.disabled = false;
|
||||
};
|
||||
|
||||
var error = function (error) {
|
||||
message.innerText = error;
|
||||
button.disabled = false;
|
||||
};
|
||||
|
||||
var process = function () {
|
||||
message.innerText = "Processing...";
|
||||
button.disabled = true;
|
||||
generateRsaKeys().then(success, error);
|
||||
};
|
||||
|
||||
var warn = function () {
|
||||
if (privateKey.value === "") return;
|
||||
return "Are you sure? Your keys will be lost unless you've saved them.";
|
||||
};
|
||||
|
||||
button.addEventListener("click", process);
|
||||
window.onbeforeunload = warn;
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -148,4 +148,7 @@
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
{% block extra %}
|
||||
{% endblock %}
|
||||
|
||||
</html>
|
||||
|
||||
0
templates/message.html
Normal file
0
templates/message.html
Normal file
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<a>
|
||||
<a href="/message/{{user.uid}}">
|
||||
<span class="icon is-large">
|
||||
<span class="icon">
|
||||
{% include "icons/mail.svg" %}
|
||||
|
||||
Reference in New Issue
Block a user