feat(web/pwcrypt): little web application for creating LDAP accounts
This generates the format expected in `//ops/users`. Note that as of this commit I have not actually tested whether the generated hashes work, as OpenLDAP doesn't ship with a tool to do that and I have to actually use it, spin up an LDAP server and bind to it. The plan is to host this at something like `tvl.fyi/signup`. There is no plan to automatically submit the generated stuff to the repo, people still have to email us (and display their street cred). Note that currently the generated hashes have slightly different parameters than what //tools/hash-password creates. This might not matter, but it's probably still a good idea to try and explicitly set Argon2 parameters. Change-Id: Ic162afbf7fb0e05ca6efc131b3bb0a4187e28029 Reviewed-on: https://cl.tvl.fyi/c/depot/+/8776 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI Reviewed-by: flokli <flokli@flokli.de>
This commit is contained in:
parent
0f71d8f813
commit
8b637521c6
7 changed files with 1299 additions and 0 deletions
48
web/pwcrypt/src/main.html
Normal file
48
web/pwcrypt/src/main.html
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
html!{
|
||||
<div class="container">
|
||||
<h1>{"//web/pwcrypt"}</h1>
|
||||
<p>{"You can use this page to create your hashed credentials for a TVL account. Enter your desired username and password below, and send us the output you receive in order for us to create your account."}</p>
|
||||
<p>
|
||||
{"Detailed documentation about the registration process is "}
|
||||
<a href="https://code.tvl.fyi/about/docs/REVIEWS.md#registration">
|
||||
{"available here"}
|
||||
</a>
|
||||
{"."}
|
||||
</p>
|
||||
<p>{"All of this happens in your browser: Your password does not leave this site!"}</p>
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>{"Credentials:"}</legend>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{"Username:"}</label>
|
||||
<input id="username" name="username" type="text"
|
||||
oninput={link.callback(|event| input_to_message(event, Msg::SetUsername))} />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{"Email:"}</label>
|
||||
<input id="email" name="email" type="email"
|
||||
oninput={link.callback(|event| input_to_message(event, Msg::SetEmail))} />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{"Password:"}</label>
|
||||
<input id="password" name="password" type="password"
|
||||
oninput={link.callback(|event| input_to_message(event, Msg::SetPassword))} />
|
||||
</div>
|
||||
|
||||
if let Some(missing) = self.whats_missing() {
|
||||
<p>{"Please fill in "}{missing}{"."}</p>
|
||||
} else {
|
||||
<div class="form-group">
|
||||
<button class="btn btn-default" type="button"
|
||||
onclick={link.callback(|_| Msg::UpdateCredentials)}>{"Prepare credentials"}</button>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
</form>
|
||||
{self.display_credentials()}
|
||||
</div>
|
||||
}
|
||||
160
web/pwcrypt/src/main.rs
Normal file
160
web/pwcrypt/src/main.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
use argon2::password_hash::{PasswordHasher, SaltString};
|
||||
use argon2::Argon2;
|
||||
use gloo::console::log;
|
||||
use rand_core::OsRng;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
|
||||
fn hash_password(pw: &str) -> String {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
argon2
|
||||
.hash_password(pw.as_bytes(), &salt)
|
||||
.expect("failed to hash pw")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
enum Msg {
|
||||
NoOp,
|
||||
SetEmail(String),
|
||||
SetPassword(String),
|
||||
SetUsername(String),
|
||||
UpdateCredentials,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
email: Option<String>,
|
||||
password: Option<String>,
|
||||
username: Option<String>,
|
||||
hashed: Option<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn whats_missing(&self) -> Option<String> {
|
||||
let mut missing = vec![];
|
||||
|
||||
if self.username.is_none() {
|
||||
missing.push("username");
|
||||
}
|
||||
|
||||
if self.email.is_none() {
|
||||
missing.push("email");
|
||||
}
|
||||
|
||||
if self.password.is_none() {
|
||||
missing.push("password");
|
||||
}
|
||||
|
||||
match missing.len() {
|
||||
0 => None,
|
||||
1 => Some(missing[0].to_string()),
|
||||
2 => Some(format!("{} and {}", missing[0], missing[1])),
|
||||
3 => Some(format!("{}, {} and {}", missing[0], missing[1], missing[2])),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_credentials(&mut self) {
|
||||
if self.password.is_none() {
|
||||
log!("error: password unset, but credentials requested");
|
||||
return;
|
||||
}
|
||||
|
||||
let pw = self.password.as_ref().unwrap();
|
||||
let hashed = hash_password(pw);
|
||||
log!("hashed password to", &hashed);
|
||||
self.hashed = Some(hashed);
|
||||
}
|
||||
|
||||
fn display_credentials(&self) -> Html {
|
||||
if let (Some(username), Some(email), Some(hash)) =
|
||||
(&self.username, &self.email, &self.hashed)
|
||||
{
|
||||
html! {
|
||||
<>
|
||||
<hr />
|
||||
<p>{"Your credentials are as follows: "}</p>
|
||||
<pre>
|
||||
{" {\n"}
|
||||
{" username = \""}{username}{"\";\n"}
|
||||
{" email = \""}{email}{"\";\n"}
|
||||
{" password = \"{ARGON2}"}{hash}{"\";\n"}
|
||||
{" }"}
|
||||
</pre>
|
||||
<p>
|
||||
{"Please propose a CL to "}
|
||||
<a href="https://at.tvl.fyi/?q=//ops/users/default.nix">
|
||||
<code>{"//ops/users/default.nix"}</code>
|
||||
</a>
|
||||
{", or submit your patch via email to "}
|
||||
<a href="mailto:depot@tvl.su">{"depot@tvl.su"}</a>
|
||||
{"."}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn input_to_message(event: InputEvent, msg: fn(String) -> Msg) -> Msg {
|
||||
let input = event.target_unchecked_into::<HtmlInputElement>();
|
||||
if input.check_validity() {
|
||||
msg(input.value())
|
||||
} else {
|
||||
Msg::NoOp
|
||||
}
|
||||
}
|
||||
|
||||
fn set_if_present(s: String, target: &mut Option<String>) {
|
||||
if s.is_empty() {
|
||||
*target = None;
|
||||
} else {
|
||||
*target = Some(s);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
log!(
|
||||
"handling message ",
|
||||
match msg {
|
||||
Msg::NoOp => "NoOp",
|
||||
Msg::SetEmail(_) => "SetEmail",
|
||||
Msg::SetUsername(_) => "SetUsername",
|
||||
Msg::SetPassword(_) => "SetPassword",
|
||||
Msg::UpdateCredentials => "UpdateCredentials",
|
||||
}
|
||||
);
|
||||
|
||||
match msg {
|
||||
Msg::NoOp => return false,
|
||||
Msg::SetEmail(email) => set_if_present(email, &mut self.email),
|
||||
Msg::SetUsername(username) => set_if_present(username, &mut self.username),
|
||||
Msg::SetPassword(password) => set_if_present(password, &mut self.password),
|
||||
Msg::UpdateCredentials => {
|
||||
self.update_credentials();
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
include!("main.html")
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue