mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-20 03:52:17 +02:00
Implement username generation, hashing, and proofs
This commit is contained in:
parent
1d358c3432
commit
731964f468
96
Cargo.lock
generated
96
Cargo.lock
generated
@ -223,6 +223,21 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -645,6 +660,12 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@ -1382,6 +1403,26 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"quick-error 2.0.1",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.9.0"
|
||||
@ -1435,6 +1476,18 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.10"
|
||||
@ -1515,6 +1568,15 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.5.1"
|
||||
@ -1596,6 +1658,18 @@ dependencies = [
|
||||
"semver 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
@ -1934,6 +2008,19 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99c0ec316ab08201476c032feb2f94a5c8ece5b209765c1fbc4430dd6e931ad6"
|
||||
|
||||
[[package]]
|
||||
name = "usernames"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"lazy_static",
|
||||
"poksho",
|
||||
"proptest",
|
||||
"rand 0.7.3",
|
||||
"sha2",
|
||||
"zkgroup",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.1.2"
|
||||
@ -1956,6 +2043,15 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
|
@ -5,6 +5,7 @@ members = [
|
||||
"rust/device-transfer",
|
||||
"rust/poksho",
|
||||
"rust/protocol",
|
||||
"rust/usernames",
|
||||
"rust/zkgroup",
|
||||
"rust/bridge/ffi",
|
||||
"rust/bridge/jni",
|
||||
@ -15,6 +16,7 @@ default-members = [
|
||||
"rust/device-transfer",
|
||||
"rust/poksho",
|
||||
"rust/protocol",
|
||||
"rust/usernames",
|
||||
"rust/zkgroup",
|
||||
]
|
||||
resolver = "2" # so that our dev-dependency features don't leak into products
|
||||
|
@ -11,6 +11,7 @@ as a Java, Swift, or TypeScript library. The underlying implementations are writ
|
||||
- attest: Functionality for remote attestation of [SGX enclaves][] and server-side [HSMs][].
|
||||
- zkgroup: Functionality for [zero-knowledge groups][] and related features available in Signal.
|
||||
- poksho: Utilities for implementing zero-knowledge proofs (such as those used by zkgroup); stands for "proof-of-knowledge, stateful-hash-object".
|
||||
- usernames: Functionality for username generation, hashing, and proofs.
|
||||
|
||||
This repository is used by the Signal client apps ([Android][], [iOS][], and [Desktop][]) as well as
|
||||
server-side. Use outside of Signal is unsupported. In particular, the products of this repository
|
||||
@ -136,6 +137,6 @@ Administration Regulations, Section 740.13) for both object code and source code
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2020-2022 Signal Messenger, LLC.
|
||||
Copyright 2020-2023 Signal Messenger, LLC.
|
||||
|
||||
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
29
rust/usernames/Cargo.toml
Normal file
29
rust/usernames/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
||||
#
|
||||
# Copyright (C) 2023 Signal Messenger, LLC.
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
[package]
|
||||
name = "usernames"
|
||||
authors = ["Signal Messenger LLC"]
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
description = "A zero-knowledge usernames library"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[dependencies]
|
||||
poksho = { path = "../poksho" }
|
||||
|
||||
sha2 = "0.9.0"
|
||||
lazy_static = "1.4.0"
|
||||
rand = "0.7.3"
|
||||
|
||||
[dependencies.curve25519-dalek]
|
||||
features = ["serde"]
|
||||
version = "3.0.0"
|
||||
git = "https://github.com/signalapp/curve25519-dalek.git"
|
||||
branch = "lizard2"
|
||||
|
||||
[dev-dependencies]
|
||||
zkgroup = { path = "../zkgroup" }
|
||||
proptest = "1.0"
|
64
rust/usernames/src/constants.rs
Normal file
64
rust/usernames/src/constants.rs
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
use std::ops::Range;
|
||||
|
||||
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub(crate) static ref BASE_POINTS: [RistrettoPoint; 3] =
|
||||
COMPRESSED_BASE_POINTS_RAW.map(|bytes| {
|
||||
let compressed = CompressedRistretto::from_slice(&bytes);
|
||||
compressed.decompress().unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
const COMPRESSED_BASE_POINTS_RAW: [[u8; 32]; 3] = [
|
||||
[
|
||||
0x60, 0xb9, 0x93, 0x66, 0x3a, 0x3d, 0xae, 0xcc, 0x4c, 0x85, 0x2f, 0x53, 0x35, 0x47, 0xe3,
|
||||
0x5, 0x38, 0x8c, 0x2a, 0x50, 0xa5, 0x83, 0x93, 0xea, 0x27, 0x7d, 0xe4, 0xab, 0xf3, 0xde,
|
||||
0x54, 0x3a,
|
||||
],
|
||||
[
|
||||
0xf2, 0xb6, 0xf1, 0xc8, 0x26, 0xfa, 0x36, 0x40, 0x20, 0x6f, 0x3b, 0x58, 0xb2, 0x28, 0x6b,
|
||||
0xde, 0xfd, 0xfd, 0xa6, 0xa5, 0x4f, 0xf9, 0x2, 0xf2, 0x4, 0xa7, 0x2d, 0xe7, 0x37, 0xd2,
|
||||
0x61, 0x57,
|
||||
],
|
||||
[
|
||||
0x6, 0x6, 0xbd, 0x3a, 0xbf, 0xce, 0x4e, 0x96, 0x17, 0xd4, 0x48, 0xfb, 0x2c, 0xae, 0xb6,
|
||||
0xcc, 0x2, 0x8e, 0xc9, 0xa2, 0xb6, 0x2b, 0x10, 0xb3, 0xd9, 0xeb, 0x29, 0x48, 0xda, 0x6f,
|
||||
0x3f, 0x53,
|
||||
],
|
||||
];
|
||||
|
||||
// 37^48 will overflow the Scalar. See nickname_scalar implementation for details.
|
||||
pub(crate) const MAX_NICKNAME_LENGTH: usize = 48;
|
||||
pub(crate) const DISCRIMINATOR_RANGES: [Range<u32>; 8] = [
|
||||
1..100,
|
||||
100..1_000,
|
||||
1_000..10_000,
|
||||
10_000..100_000,
|
||||
100_000..1_000_000,
|
||||
1_000_000..10_000_000,
|
||||
10_000_000..100_000_000,
|
||||
100_000_000..1_000_000_000,
|
||||
];
|
||||
|
||||
pub(crate) const CANDIDATES_PER_RANGE: [u8; 8] = [4, 3, 3, 2, 2, 2, 2, 2];
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use zkgroup::common::sho::Sho;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_points() {
|
||||
let mut sho = Sho::new(b"Signal_Username_20230130_Constant_Points_Generate", b"");
|
||||
for p in BASE_POINTS.iter() {
|
||||
assert_eq!(&sho.get_point(), p);
|
||||
}
|
||||
}
|
||||
}
|
14
rust/usernames/src/error.rs
Normal file
14
rust/usernames/src/error.rs
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UsernameError {
|
||||
BadUsernameFormat,
|
||||
BadDiscriminator,
|
||||
BadNicknameCharacter,
|
||||
NicknameTooShort,
|
||||
NicknameTooLong,
|
||||
ProofVerificationFailure,
|
||||
}
|
11
rust/usernames/src/lib.rs
Normal file
11
rust/usernames/src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
mod username;
|
||||
|
||||
pub use error::UsernameError;
|
||||
pub use username::*;
|
410
rust/usernames/src/username.rs
Normal file
410
rust/usernames/src/username.rs
Normal file
@ -0,0 +1,410 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::ops::{Add, Range, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
use lazy_static::lazy_static;
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
use poksho::args::{PointArgs, ScalarArgs};
|
||||
use poksho::{PokshoError, Statement};
|
||||
|
||||
use crate::constants::{
|
||||
BASE_POINTS, CANDIDATES_PER_RANGE, DISCRIMINATOR_RANGES, MAX_NICKNAME_LENGTH,
|
||||
};
|
||||
use crate::error::UsernameError;
|
||||
|
||||
lazy_static! {
|
||||
static ref PROOF_STATEMENT: Statement = {
|
||||
let mut st = Statement::new();
|
||||
st.add(
|
||||
"username_hash",
|
||||
&[
|
||||
("username_sha_scalar", "G1"),
|
||||
("nickname_scalar", "G2"),
|
||||
("discriminator_scalar", "G3"),
|
||||
],
|
||||
);
|
||||
st
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Username {
|
||||
nickname: String,
|
||||
discriminator: u64,
|
||||
scalars: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl Display for Username {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}.{}", self.nickname, self.discriminator)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Username {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Username")
|
||||
.field("nickname", &self.nickname)
|
||||
.field("discriminator", &self.discriminator)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NicknameLimits(RangeInclusive<usize>);
|
||||
|
||||
impl Default for NicknameLimits {
|
||||
fn default() -> Self {
|
||||
NicknameLimits::new(3, 32).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl NicknameLimits {
|
||||
pub fn new(min_len: usize, max_len: usize) -> Result<Self, UsernameError> {
|
||||
assert!(
|
||||
max_len <= MAX_NICKNAME_LENGTH,
|
||||
"Long nicknames are not supported. The maximum supported length is {}",
|
||||
MAX_NICKNAME_LENGTH
|
||||
);
|
||||
assert!(
|
||||
min_len < max_len,
|
||||
"Invalid nickname size limits: {}..{}",
|
||||
min_len,
|
||||
max_len
|
||||
);
|
||||
Ok(NicknameLimits(min_len..=max_len))
|
||||
}
|
||||
|
||||
pub fn validate(&self, n: usize) -> Result<(), UsernameError> {
|
||||
if &n < self.0.start() {
|
||||
return Err(UsernameError::NicknameTooShort);
|
||||
}
|
||||
if &n > self.0.end() {
|
||||
return Err(UsernameError::NicknameTooLong);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Username {
|
||||
pub fn new(s: &str) -> Result<Self, UsernameError> {
|
||||
let (original_nickname, suffix) =
|
||||
s.rsplit_once('.').ok_or(UsernameError::BadUsernameFormat)?;
|
||||
let nickname = original_nickname.to_ascii_lowercase();
|
||||
let discriminator = validate_discriminator(suffix)?;
|
||||
|
||||
let scalars = make_scalars(&nickname, discriminator)?;
|
||||
Ok(Self {
|
||||
nickname: original_nickname.to_string(),
|
||||
discriminator,
|
||||
scalars,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
*Self::hash_from_scalars(&self.scalars).compress().as_bytes()
|
||||
}
|
||||
|
||||
pub fn proof(&self, randomness: &[u8]) -> Result<Vec<u8>, UsernameError> {
|
||||
let hash = Self::hash_from_scalars(&self.scalars);
|
||||
let scalar_args = Self::make_scalar_args(&self.scalars);
|
||||
let point_args = Self::make_point_args(hash);
|
||||
let message = *hash.compress().as_bytes();
|
||||
PROOF_STATEMENT
|
||||
.prove(&scalar_args, &point_args, &message, randomness)
|
||||
.map_err(|e| panic!("Failed to create proof. Cause: PokshoError::{:?}", e))
|
||||
}
|
||||
|
||||
pub fn verify_proof(proof: &[u8], hash: [u8; 32]) -> Result<(), UsernameError> {
|
||||
let hash_point = CompressedRistretto(hash)
|
||||
.decompress()
|
||||
.ok_or(UsernameError::ProofVerificationFailure)?;
|
||||
let point_args = Self::make_point_args(hash_point);
|
||||
PROOF_STATEMENT
|
||||
.verify_proof(proof, &point_args, &hash)
|
||||
.map_err(|e| match e {
|
||||
PokshoError::VerificationFailure => UsernameError::ProofVerificationFailure,
|
||||
_ => panic!("Unexpected verification error PokshoError::{:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn candidates_from<R: Rng>(
|
||||
rng: &mut R,
|
||||
nickname: &str,
|
||||
limits: NicknameLimits,
|
||||
) -> Result<Vec<String>, UsernameError> {
|
||||
validate_nickname(nickname, &limits)?;
|
||||
let candidates = random_discriminators(rng, &CANDIDATES_PER_RANGE, &DISCRIMINATOR_RANGES)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|d| format!("{}.{:0>2}", nickname, d))
|
||||
.collect();
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
fn hash_from_scalars(scalars: &[Scalar]) -> RistrettoPoint {
|
||||
BASE_POINTS
|
||||
.iter()
|
||||
.zip(scalars)
|
||||
.map(|(point, scalar)| point * scalar)
|
||||
.reduce(RistrettoPoint::add)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn make_scalar_args(scalars: &[Scalar]) -> ScalarArgs {
|
||||
let mut args = ScalarArgs::new();
|
||||
for (scalar, name) in scalars.iter().zip([
|
||||
"username_sha_scalar",
|
||||
"nickname_scalar",
|
||||
"discriminator_scalar",
|
||||
]) {
|
||||
args.add(name, *scalar);
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
fn make_point_args(lhs: RistrettoPoint) -> PointArgs {
|
||||
let mut args = PointArgs::new();
|
||||
for (idx, point) in BASE_POINTS.iter().enumerate() {
|
||||
let name = format!("G{}", idx + 1);
|
||||
args.add(&name, *point);
|
||||
}
|
||||
args.add("username_hash", lhs);
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
fn username_sha_scalar(nickname: &str, discriminator: u64) -> Result<Scalar, UsernameError> {
|
||||
let mut hash = Sha512::new();
|
||||
hash.update(nickname.as_bytes());
|
||||
hash.update([0x00]);
|
||||
hash.update(discriminator.to_be_bytes());
|
||||
Ok(Scalar::from_hash(hash))
|
||||
}
|
||||
|
||||
fn nickname_scalar(nickname: &str) -> Result<Scalar, UsernameError> {
|
||||
let bytes: Option<Vec<u8>> = nickname.chars().map(char_to_byte).collect();
|
||||
bytes
|
||||
.map(|b| to_base_37_scalar(&b))
|
||||
.ok_or(UsernameError::BadNicknameCharacter)
|
||||
}
|
||||
|
||||
fn discriminator_scalar(discriminator: u64) -> Result<Scalar, UsernameError> {
|
||||
Ok(Scalar::from(discriminator))
|
||||
}
|
||||
|
||||
fn make_scalars(nickname: &str, discriminator: u64) -> Result<Vec<Scalar>, UsernameError> {
|
||||
Ok(vec![
|
||||
username_sha_scalar(nickname, discriminator)?,
|
||||
nickname_scalar(nickname)?,
|
||||
discriminator_scalar(discriminator)?,
|
||||
])
|
||||
}
|
||||
|
||||
// The mapping is only defined for the characters matching [_0-9a-z]
|
||||
fn char_to_byte(c: char) -> Option<u8> {
|
||||
match c {
|
||||
'_' => Some(1),
|
||||
'a'..='z' => Some(c as u8 - b'a' + 2),
|
||||
'0'..='9' => Some(c as u8 - b'0' + 28),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_base_37_scalar(bytes: &[u8]) -> Scalar {
|
||||
let thirty_seven = Scalar::from(37u8);
|
||||
let mut scalar = Scalar::zero();
|
||||
for b in bytes.iter().skip(1).rev() {
|
||||
scalar *= thirty_seven;
|
||||
scalar += Scalar::from(*b);
|
||||
}
|
||||
scalar *= Scalar::from(27u8);
|
||||
scalar += Scalar::from(bytes[0]);
|
||||
scalar
|
||||
}
|
||||
|
||||
fn validate_discriminator<T: FromStr + PartialOrd + From<u8>>(
|
||||
discriminator: &str,
|
||||
) -> Result<T, UsernameError> {
|
||||
let n = T::from_str(discriminator).unwrap_or_else(|_| T::from(0));
|
||||
if n == T::from(0) {
|
||||
return Err(UsernameError::BadDiscriminator);
|
||||
}
|
||||
if n < T::from(10) && discriminator.len() != 2 {
|
||||
return Err(UsernameError::BadDiscriminator);
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn validate_nickname(nickname: &str, limits: &NicknameLimits) -> Result<(), UsernameError> {
|
||||
let maybe_bytes: Option<Vec<_>> = nickname
|
||||
.to_ascii_lowercase()
|
||||
.chars()
|
||||
.map(char_to_byte)
|
||||
.collect();
|
||||
|
||||
let bytes = maybe_bytes.ok_or(UsernameError::BadNicknameCharacter)?;
|
||||
|
||||
limits.validate(bytes.len())
|
||||
}
|
||||
|
||||
fn random_discriminators<R: Rng>(
|
||||
rng: &mut R,
|
||||
count_per_range: &[u8],
|
||||
ranges: &[Range<u32>],
|
||||
) -> Result<Vec<u32>, UsernameError> {
|
||||
assert!(count_per_range.len() <= ranges.len(), "Not enough ranges");
|
||||
let total_count: u8 = count_per_range.iter().sum();
|
||||
let mut results = Vec::with_capacity(total_count as usize);
|
||||
for (n, range) in count_per_range.iter().zip(ranges) {
|
||||
results.extend((0..*n).map(|_| rng.gen_range(range.start, range.end)));
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
const NICKNAME_PATTERN: &str = "[_a-z][_a-z0-9]{2,31}";
|
||||
const DISCRIMINATOR_MAX: u64 = 1_000_000_000_u64;
|
||||
|
||||
#[test]
|
||||
fn valid_nickname_scalar() {
|
||||
// the results should be 1 + 27*27 + 37*27*37^1 + 1*27*37^2 = 74656
|
||||
let nickname = "_z9_";
|
||||
assert_eq!(Scalar::from(74656_u32), nickname_scalar(nickname).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_usernames() {
|
||||
for username in ["He110.01", "usr.999999999", "_identifier.42"] {
|
||||
Username::new(username).map(|name| name.hash()).unwrap();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn invalid_usernames() {
|
||||
for username in [
|
||||
"no_discriminator",
|
||||
"🦀.42",
|
||||
"s p a c e s.01",
|
||||
"zero.00",
|
||||
"zeropad.001",
|
||||
"short.1",
|
||||
"short_zero.0",
|
||||
] {
|
||||
assert!(
|
||||
Username::new(username).map(|n| n.hash()).is_err(),
|
||||
"Unexpected success for username '{}'",
|
||||
username
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_characters_mapping() {
|
||||
let all_valid: Option<Vec<_>> = "_abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
.chars()
|
||||
.map(char_to_byte)
|
||||
.collect();
|
||||
let unwrapped = all_valid.expect("char_to_byte defined for all valid characters");
|
||||
let sorted = {
|
||||
let mut xs = unwrapped.clone();
|
||||
xs.sort();
|
||||
xs
|
||||
};
|
||||
assert_eq!(sorted, unwrapped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_nicknames_should_produce_scalar() {
|
||||
proptest!(|(nickname in NICKNAME_PATTERN)| {
|
||||
nickname_scalar(&nickname).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_usernames_should_produce_scalar() {
|
||||
proptest!(|(nickname in NICKNAME_PATTERN, discriminator in 1..DISCRIMINATOR_MAX)| {
|
||||
username_sha_scalar(&nickname, discriminator).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_scalar_is_defined_on_range() {
|
||||
proptest!(|(n in 1..DISCRIMINATOR_MAX)| {
|
||||
discriminator_scalar(n).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_usernames_proof_and_verify() {
|
||||
proptest!(|(nickname in NICKNAME_PATTERN, discriminator in 1..DISCRIMINATOR_MAX)| {
|
||||
let username = Username::new(&format!("{nickname}.{discriminator:0>2}")).unwrap();
|
||||
let hash = username.hash();
|
||||
let randomness: Vec<u8> = (1..33).collect();
|
||||
let proof = username.proof(&randomness).unwrap();
|
||||
Username::verify_proof(&proof, hash).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_random_makes_valid_usernames() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let randomness: Vec<u8> = (1..33).collect();
|
||||
let nickname = "_SiGNA1";
|
||||
let candidates = Username::candidates_from(&mut rng, nickname, Default::default()).unwrap();
|
||||
for c in &candidates {
|
||||
assert!(c.starts_with(nickname));
|
||||
let username = Username::new(c).unwrap();
|
||||
let hash = username.hash();
|
||||
let proof = username.proof(&randomness).unwrap();
|
||||
Username::verify_proof(&proof, hash).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_discriminators() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let ds = random_discriminators(&mut rng, &[4, 3, 2, 1], &DISCRIMINATOR_RANGES).unwrap();
|
||||
assert!(DISCRIMINATOR_RANGES[0].contains(&ds[0]));
|
||||
assert!(DISCRIMINATOR_RANGES[0].contains(&ds[1]));
|
||||
assert!(DISCRIMINATOR_RANGES[0].contains(&ds[2]));
|
||||
assert!(DISCRIMINATOR_RANGES[0].contains(&ds[3]));
|
||||
assert!(DISCRIMINATOR_RANGES[1].contains(&ds[4]));
|
||||
assert!(DISCRIMINATOR_RANGES[1].contains(&ds[5]));
|
||||
assert!(DISCRIMINATOR_RANGES[1].contains(&ds[6]));
|
||||
assert!(DISCRIMINATOR_RANGES[2].contains(&ds[7]));
|
||||
assert!(DISCRIMINATOR_RANGES[2].contains(&ds[8]));
|
||||
assert!(DISCRIMINATOR_RANGES[3].contains(&ds[9]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn too_few_ranges() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let counts: Vec<u8> = (0u8..DISCRIMINATOR_RANGES.len() as u8 + 1).collect();
|
||||
let _ = random_discriminators(&mut rng, &counts, &DISCRIMINATOR_RANGES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nickname_limits() {
|
||||
NicknameLimits::default(); // should not panic
|
||||
NicknameLimits::new(0, 42).unwrap().validate(13).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn invalid_nickname_limits() {
|
||||
let _ = NicknameLimits::new(42, 0);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user