commit d68beaeaef586438bb3000177963b27ed751ce1c
parent 348e209ce06e6a2d7ff368a793553bc7f19a59b1
Author: seestem <seestem@noreply.codeberg.org>
Date: Sun, 21 Aug 2022 14:38:20 +0200
Merge pull request 'accounts' (#2) from accounts into root
Reviewed-on: https://codeberg.org/seestem/cyrtophora/pulls/2
Diffstat:
7 files changed, 234 insertions(+), 410 deletions(-)
diff --git a/README.md b/README.md
@@ -13,24 +13,8 @@ ___
░▀▀▀░░▀░░▀░▀░░▀░░▀▀▀░▀░░░▀░▀░▀▀▀░▀░▀░▀░▀
```
-User management protocol. The philosophy is that with the right
-cryptography and data handling strategies you don't need to
-self-host to have freedom/control and privacy.
-
-## Features
-
-- [ ] Confidentiality
-- [ ] Integrity
-- [ ] Availability
-- [ ] Authentication
-- [ ] Validation
-- [ ] Session management
-- [ ] Lock-in Free
-
-## Cyphers
-
Cyrtophora is built on top of the following cyphers:
- [scrypt](https://www.tarsnap.com/scrypt.html): Used for password hashing.
- [ed25519](https://ed25519.cr.yp.to/): Digital Signatures
-- [XChaCha20-Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction): Symmetric Encryption
+- [XChaCha20-Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction): Symmetric Encryption
diff --git a/doc/accounts.md b/doc/accounts.md
@@ -0,0 +1,37 @@
+# Accounts
+
+```rust
+pub struct Account {
+ /// The username of the user, also used as an unique identifier
+ pub username: String,
+ /// The password of the user
+ pub password: String,
+ /// The email address the user
+ pub email: Option<String>,
+}
+```
+
+## Mandatory Fields
+
+An `Account` contains two mandatory fields.
+
+### `username`
+
+Twitter style username, used as a unique user identifier.
+
+- cannot start with a underscore (_)
+- can only contain letters, numbers, and one underscore
+- can have only one (_) underscore
+
+
+### `password`
+
+Users master key.
+
+- should be at least 10 characters long
+
+## Optional Fields
+
+### `email`
+
+The email address the user.
diff --git a/doc/style.css b/doc/style.css
@@ -1,202 +0,0 @@
-
-html {
- line-height: 1.5;
- font-family: Georgia, serif;
- font-size: 20px;
- color: #ffffff;
- background-color: #222D31 ;
-}
-body {
- margin: 0 auto;
- max-width: 36em;
- padding-left: 50px;
- padding-right: 50px;
- padding-top: 0px;
- margin-top: 0px;
- padding-bottom: 50px;
- hyphens: auto;
- word-wrap: break-word;
- text-rendering: optimizeLegibility;
- font-kerning: normal;
-}
-
-@media (max-width: 600px) {
- body {
- font-size: 0.9em;
- padding: 1em;
- }
-}
-@media print {
- body {
- background-color: transparent;
- color: black;
- font-size: 12pt;
- }
- p, h2, h3 {
- orphans: 3;
- widows: 3;
- }
- h2, h3, h4 {
- page-break-after: avoid;
- }
- #TOC{
- position: relative !important;
- width: inherit;
- height: inherit;
- border: none;
- }
-}
-p {
- margin: 1em 0;
-}
-a {
- color: cyan;
-}
-a:visited {
- color: cyan;
-}
-img {
- max-width: 100%;
-}
-h1, h2, h3, h4, h5, h6 {
- margin-top: 1.4em;
- color: cyan;
-}
-h5, h6 {
- font-size: 1em;
- font-style: italic;
-}
-h6 {
- font-weight: normal;
-}
-ol, ul {
- padding-left: 1.7em;
- margin-top: 1em;
-}
-li > ol, li > ul {
- margin-top: 0;
-}
-blockquote {
- margin: 1em 0 1em 1.7em;
- padding-left: 1em;
- border-left: 2px solid #e6e6e6;
- color: #606060;
-}
-code {
- font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace;
- font-size: 85%;
- margin: 0;
-}
-pre {
- margin: 1em 0;
- overflow: auto;
-}
-pre code {
- padding: 0;
- overflow: visible;
-}
-.sourceCode {
- background-color: transparent;
- overflow: visible;
-}
-
-hr {
- background-color: cyan;
- border: none;
- height: 2px;
- margin: 4em 0;
-}
-table {
- margin: 1em 0;
- border-collapse: collapse;
- width: 100%;
- overflow-x: auto;
- display: block;
- font-variant-numeric: lining-nums tabular-nums;
-}
-table caption {
- margin-bottom: 0.75em;
-}
-tbody {
- margin-top: 0.5em;
- border-top: 1px solid cyan;
- border-bottom: 1px solid cyan;
-}
-th {
- border-top: 1px solid cyan;
- padding: 0.25em 0.5em 0.25em 0.5em;
-}
-td {
- padding: 0.125em 0.5em 0.25em 0.5em;
-}
-header {
- margin-bottom: 4em;
- text-align: center;
-}
-
-#TOC{
- position: fixed;
- width: 20vw;
- height: 100vh;
- left: 0;
- top: 0;
- border-right: thin solid cyan;
- padding-left: 1em;
- overflow: auto;
-}
-
-#TOC ul {
- padding-left: .5em;
-}
-
-#TOC li {
- list-style: circle;
- width: 100%
-}
-
-#TOC a:not(:hover) {
- text-decoration: none;
-}
-
-pre{
- border: thin solid cyan;
- padding: 1em;
- font-family: monospace !Important;
- font-size: 18px;
-}
-
-blockquote{
- border-left: 2px solid cyan;
- color: #7E807E;
-}
-
-@media only screen and (max-width: 1300px) {
- #TOC{
- position: relative !important;
- width: inherit;
- height: inherit;
- border: none;
- }
-
- pre{
- font-size: 16px;
- }
-}
-
-@media only screen and (max-width: 480px) {
- pre{
- font-size: 12px;
- }
-}
-
-@media only screen and (max-width: 370px) {
- pre{
- font-size: 10px;
- }
-}
-
-@media only screen and (max-width: 300px) {
- pre{
- font-size: 7px;
- }
-}
diff --git a/src/account.rs b/src/account.rs
@@ -0,0 +1,47 @@
+use crate::validate::{Validate, ValidationError};
+
+/// An account
+pub struct Account {
+ /// The username of the user, also used as an unique identifier
+ pub username: String,
+ /// The password of the user
+ pub password: String,
+ /// The email address the user
+ pub email: Option<String>,
+}
+
+impl Account {
+ /// Register new user
+ pub fn register(
+ username: &str,
+ password: &str,
+ email: Option<&str>,
+ ) -> Result<Self, ValidationError> {
+ let mut validated_email: Option<String> = None;
+
+ if !Validate::username(username) {
+ return Err(ValidationError::Username);
+ }
+
+ if !Validate::password(password) {
+ return Err(ValidationError::Email);
+ }
+
+ if let Some(email) = email {
+ if !Validate::email(email) {
+ return Err(ValidationError::Email);
+ }
+ validated_email = Some(email.to_string());
+
+ // TODO: email verification code
+ }
+
+ // TODO: database registration code
+
+ Ok(Account {
+ username: username.to_string(),
+ email: validated_email,
+ password: password.to_string(),
+ })
+ }
+}
diff --git a/src/lib.rs b/src/crypto.rs
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,191 +1,3 @@
-use anyhow::Result;
-use chacha20poly1305::{
- aead::{Aead, NewAead},
- Key, XChaCha20Poly1305, XNonce,
-};
-use ring::{
- digest::{self, digest, Digest},
- rand::SystemRandom,
- signature::{Ed25519KeyPair, KeyPair},
-};
-use scrypt::{
- password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
- Scrypt,
-};
-
-const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN;
-
-/// Nonce used when encrypting the Crypto's keypair (Crypto::encrypt_keypair_document())
-const AED_NONCE: &str = "people the information ";
-pub type Credential = [u8; CREDENTIAL_LEN];
-
-pub struct Credentials {
- /// Base64 encoded key pair serialized as a PKCS#8 document
- pub keypair_doc: String,
- /// Base64 encoded public key
- pub public_key: String,
-}
-
-pub struct Crypto {}
-
-impl Crypto {
- /// Generate Ed25519 key pair, for signing
- pub fn gen_ed25519_document() -> Result<Credentials> {
- let rng = SystemRandom::new();
- // TODO: don't use expect()
- let keypair_document =
- Ed25519KeyPair::generate_pkcs8(&rng).expect("Could not generate Ed25519KeyPair");
- let keypair = Ed25519KeyPair::from_pkcs8(keypair_document.as_ref())
- .expect("Could not decode keypair from keypair document");
- let public_key_string = base64::encode(keypair.public_key().as_ref());
- let keypair_document_string = base64::encode(keypair_document.as_ref());
- let cred = Credentials {
- keypair_doc: keypair_document_string,
- public_key: public_key_string,
- };
-
- Ok(cred)
- }
-
- /// Generate scrypt based hashed password
- pub fn hash_password(password: &str) -> Result<String> {
- let salt = SaltString::generate(&mut OsRng);
- Ok(Scrypt
- .hash_password(password.as_bytes(), &salt)?
- .to_string())
- }
-
- /// Verify scrypt based password hash
- fn verify_password_hash(attempted_password: &str, hash: &str) -> Result<bool> {
- let parsed_hash = PasswordHash::new(hash)?;
-
- if Scrypt
- .verify_password(attempted_password.as_bytes(), &parsed_hash)
- .is_ok()
- {
- Ok(true)
- } else {
- Ok(false)
- }
- }
-
- /// Encrypt the keypair (ed25519) document, using XChaCha20Poly1305
- /// so that it can be safely persisted into the database. The
- /// Crypto's login password is used a the secret key.
- pub fn encrypt_keypair_document(keypair_document: &str, password: &str) -> Result<String> {
- let secret_key = Crypto::gen_secret_key(password);
- let key = Key::from_slice(&secret_key.as_ref()[..32]); // 32-bytes
- let aead = XChaCha20Poly1305::new(key);
- let nonce = XNonce::from_slice(AED_NONCE.as_bytes()); // 24-bytes; unique
- let keypair_document_bytes = base64::decode(keypair_document)?;
- let res = base64::encode(
- aead.encrypt(nonce, keypair_document_bytes.as_ref())
- .expect("encryption failure!"),
- );
- Ok(res)
- }
-
- /// Decrypt the encrypted keypair (ed25519), using XChaCha20Poly1305
- fn decrypt_keypair_document(cyphertext: &str, password: &str) -> Result<String> {
- let secret_key = Crypto::gen_secret_key(password);
- let key = Key::from_slice(&secret_key.as_ref()[..32]); // 32-bytes
- let nonce = XNonce::from_slice(AED_NONCE.as_bytes()); // 24-bytes; unique
- let aead = XChaCha20Poly1305::new(key);
- let cyphertext_bytes = base64::decode(cyphertext)?;
- let res = base64::encode(
- aead.decrypt(nonce, cyphertext_bytes.as_ref())
- .expect("decryption failure!"),
- );
- Ok(res)
- }
-
- /// Generate secret key, used to encrypt the ed25519 keypair
- fn gen_secret_key(password: &str) -> Digest {
- digest(&digest::SHA256, password.as_bytes())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use ring::signature::{Ed25519KeyPair, KeyPair, UnparsedPublicKey, ED25519};
-
- #[test]
- fn hash_password() {
- let password1 = "a bad password";
- let hash1 = Crypto::hash_password(password1).unwrap();
-
- let password2 = "a bad password";
- let hash2 = Crypto::hash_password(password2).unwrap();
-
- // The generated hashes are all 88 bytes long
- assert_eq!(hash1.len(), 88);
- assert_eq!(hash2.len(), 88);
-
- // Even if the user passwords are the same they do not generate
- // the same hashes because the username is also used as a
- // seed when the hash is generated.
- assert_ne!(hash1, hash2);
-
- // Verify password
- assert!(Crypto::verify_password_hash("a bad password", &hash1).unwrap());
- assert!(!Crypto::verify_password_hash("wrong password", &hash1).unwrap());
- }
-
- #[test]
- fn gen_keypairs() {
- let cred = Crypto::gen_ed25519_document().unwrap();
- let document = base64::decode(cred.keypair_doc).unwrap();
- let key_pair = Ed25519KeyPair::from_pkcs8(&document).unwrap();
-
- // Sign the message "hello, world".
- const MESSAGE: &[u8] = b"hello, world";
- let sig = key_pair.sign(MESSAGE);
-
- // Normally an application would extract the bytes of the signature and
- // send them in a protocol message to the peer(s). Here we just get the
- // public key key directly from the key pair.
- let peer_public_key_bytes = key_pair.public_key().as_ref();
-
- // Verify the signature of the message using the public key. Normally the
- // verifier of the message would parse the inputs to this code out of the
- // protocol message(s) sent by the signer.
- let peer_public_key = UnparsedPublicKey::new(&ED25519, peer_public_key_bytes);
- peer_public_key.verify(MESSAGE, sig.as_ref()).unwrap();
- }
-
- #[test]
- fn symmetric_encryption_decryption() {
- let password = "my very weak password";
- let cred = Crypto::gen_ed25519_document().unwrap();
- let document = base64::decode(cred.keypair_doc.clone()).unwrap();
- let expected_key_pair = Ed25519KeyPair::from_pkcs8(&document).unwrap();
-
- let wrong_cred = Crypto::gen_ed25519_document().unwrap();
- let wrong_document = base64::decode(wrong_cred.keypair_doc).unwrap();
- let wrong_key_pair = Ed25519KeyPair::from_pkcs8(&wrong_document).unwrap();
-
- // encrypt the document
- let cyphertext = Crypto::encrypt_keypair_document(&cred.keypair_doc, password).unwrap();
- let cleartext = Crypto::decrypt_keypair_document(&cyphertext, password).unwrap();
-
- let cleartext_bytes = base64::decode(cleartext.clone()).unwrap();
- let key_pair = Ed25519KeyPair::from_pkcs8(cleartext_bytes.as_ref()).unwrap();
-
- assert_ne!(cyphertext, cleartext);
- assert_eq!(
- expected_key_pair.public_key().as_ref(),
- key_pair.public_key().as_ref()
- );
-
- assert_ne!(
- expected_key_pair.public_key().as_ref(),
- wrong_key_pair.public_key().as_ref()
- );
-
- assert_ne!(
- key_pair.public_key().as_ref(),
- wrong_key_pair.public_key().as_ref()
- );
- }
-}
+mod account;
+mod crypto;
+mod validate;
diff --git a/src/validate.rs b/src/validate.rs
@@ -0,0 +1,146 @@
+pub struct Validate;
+
+impl Validate {
+ /// Validate username (using Twitter style usernames)
+ pub fn username(username: &str) -> bool {
+ // Username cannot be empty
+ if username.is_empty() {
+ return false;
+ }
+
+ // Username length cannot be greater than 15 characters
+ if username.len() > 15 {
+ return false;
+ }
+
+ let chars: Vec<char> = username.chars().collect();
+ let mut underscore_count = 0;
+
+ for i in 0..chars.len() {
+ // Username cannot start with a underscore (_)
+ if i == 0 && chars[i] == '_' {
+ return false;
+ }
+
+ // Username can only contain letters, numbers, and one underscore
+ if chars[i] != '_' {
+ if !chars[i].is_ascii_alphanumeric() {
+ return false;
+ }
+ } else {
+ // Count underscores because username can have only one (_) underscore
+ underscore_count += 1;
+ }
+ }
+
+ // Username can have only one (_) underscore
+ if underscore_count > 1 {
+ return false;
+ }
+ true
+ }
+
+ /// TODO: better email validation
+ pub fn email(email: &str) -> bool {
+ // Email address cannot be empty
+ if email.is_empty() {
+ return false;
+ }
+
+ // Email address should contain @ and . symbols
+ if !email.contains('@') || !email.contains('.') {
+ return false;
+ }
+ true
+ }
+
+ /// TODO: better password validation
+ pub fn password(password: &str) -> bool {
+ // Password should be at least 10 characters long
+ if password.len() < 10 {
+ return false;
+ }
+ true
+ }
+}
+
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ValidationError {
+ Username,
+ Email,
+ Password,
+}
+
+impl std::error::Error for ValidationError {}
+
+impl fmt::Display for ValidationError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ValidationError::Username => write!(f, "Invalid username"),
+ ValidationError::Email => write!(f, "Invalid email"),
+ ValidationError::Password => write!(f, "Invalid password"),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn username_validation() {
+ let valid_username = "firephoenix";
+ let valid_username1 = "natty_dread";
+ let valid_username2 = "ironlion_ironii";
+ let invalid_username = "";
+ let invalid_username1 = "natty_dread_iron";
+ let invalid_username2 = "_natty_dread_iron";
+ let invalid_username3 = "nattydre@d";
+ let invalid_username4 = "nattydread$";
+ let invalid_username5 = "firephoenixfirephoenixfirephoenixfirephoenixfirephoenix";
+
+ assert!(Validate::username(valid_username));
+ assert!(Validate::username(valid_username1));
+ assert!(Validate::username(valid_username2));
+
+ // username cannot be empty
+ assert!(!Validate::username(invalid_username));
+ // username cannot contain more than 1 underscore
+ assert!(!Validate::username(invalid_username1));
+ // username cannot start with an underscore
+ assert!(!Validate::username(invalid_username2));
+ // username cannot contain special symbols (only the _ is allowed)
+ assert!(!Validate::username(invalid_username3));
+ assert!(!Validate::username(invalid_username4));
+ // username cannot be more than 15 characters long
+ assert!(!Validate::username(invalid_username5));
+ }
+
+ #[test]
+ fn email_validation() {
+ let valid_email = "example@example.com";
+ let valid_email1 = "test@gmail.com";
+ let invalid_email = "kdkdf dfkfd@uurt";
+ let invalid_email2 = "lolz.lolz";
+ let invalid_email3 = "lolz@lolz";
+
+ assert!(Validate::email(valid_email));
+ assert!(Validate::email(valid_email1));
+
+ assert!(!Validate::email(invalid_email));
+ assert!(!Validate::email(invalid_email2));
+ assert!(!Validate::email(invalid_email3));
+ }
+
+ #[test]
+ fn password_validation() {
+ let valid_password = "passwordShoul be at least 10 chars";
+ let invalid_password = "lolz";
+
+ assert!(Validate::password(valid_password));
+ // Password should be at least 10 characters long
+ assert!(!Validate::password(invalid_password));
+ }
+}