///|
/// A portable password policy. The policy has no dependency on ESP-IDF,
/// FoloToy, LVGL, a filesystem, or a particular random-number provider.
pub struct PasswordPolicy {
length : Int
lowercase : Bool
uppercase : Bool
digits : Bool
symbols : Bool
exclude_ambiguous : Bool
safe_symbols_only : Bool
}
///|
/// Construct a password policy for a library, command-line, web, or embedded
/// consumer. Use `PasswordPolicy::is_valid` before presenting custom values.
pub fn PasswordPolicy::new(
length : Int,
lowercase? : Bool = true,
uppercase? : Bool = true,
digits? : Bool = true,
symbols? : Bool = true,
exclude_ambiguous? : Bool = true,
safe_symbols_only? : Bool = true,
) -> PasswordPolicy {
{
length,
lowercase,
uppercase,
digits,
symbols,
exclude_ambiguous,
safe_symbols_only,
}
}
///|
/// A broadly compatible policy for sites that reject punctuation.
pub fn PasswordPolicy::compatible() -> PasswordPolicy {
PasswordPolicy::new(12, symbols=false)
}
///|
/// The recommended default for general-purpose credentials.
pub fn PasswordPolicy::standard() -> PasswordPolicy {
PasswordPolicy::new(16)
}
///|
/// A longer policy that keeps the complete supported character set.
pub fn PasswordPolicy::strict() -> PasswordPolicy {
PasswordPolicy::new(20, exclude_ambiguous=false, safe_symbols_only=false)
}
///|
pub fn PasswordPolicy::length(self : PasswordPolicy) -> Int {
self.length
}
///|
/// Return the same policy with a different output length. This preserves the
/// selected profile's character-set rules.
pub fn PasswordPolicy::with_length(
self : PasswordPolicy,
length : Int,
) -> PasswordPolicy {
{ ..self, length, }
}
///|
pub fn PasswordPolicy::is_valid(self : PasswordPolicy) -> Bool {
policy_valid(
self.length,
self.lowercase,
self.uppercase,
self.digits,
self.symbols,
)
}
///|
/// Generate a password as a MoonBit `String`. Randomness is injected so a
/// native application, browser, test, or microcontroller can provide the
/// entropy source appropriate to its platform.
pub fn generate_password(
policy : PasswordPolicy,
next_u32 : () -> UInt,
) -> Result[String, String] {
let builder = StringBuilder()
let result = generate_password_into(policy, next_u32, fn(code) {
match code.to_char() {
Some(ch) => {
builder.write_char(ch)
true
}
None => false
}
})
match result {
0 => Ok(builder.to_string())
-1 => Err("invalid password policy")
_ => Err("output rejected generated character")
}
}
///|
/// Generate a password into a caller-owned sink. This allocation-controlled
/// API is the integration point used by embedded firmware.
pub fn generate_password_into(
policy : PasswordPolicy,
next_u32 : () -> UInt,
emit : (Int) -> Bool,
) -> Int {
generate_password_compat(
next_u32,
emit,
policy.length,
policy.lowercase,
policy.uppercase,
policy.digits,
policy.symbols,
policy.exclude_ambiguous,
policy.safe_symbols_only,
)
}
///|
/// Compatibility API for C and existing callback-based consumers. New
/// MoonBit applications should prefer `PasswordPolicy` and
/// `generate_password` or `generate_password_into`.
pub fn generate_password_compat(
next_u32 : () -> UInt,
emit : (Int) -> Bool,
length : Int,
lowercase : Bool,
uppercase : Bool,
digits : Bool,
symbols : Bool,
exclude_ambiguous : Bool,
safe_symbols_only : Bool,
) -> Int {
if !policy_valid(length, lowercase, uppercase, digits, symbols) {
return -1
}
let output = FixedArray::make(length, 0)
let mut cursor = 0
if lowercase {
output[cursor] = lowercase_at(
unbiased_index(next_u32, lowercase_count(exclude_ambiguous)),
exclude_ambiguous,
)
cursor = cursor + 1
}
if uppercase {
output[cursor] = uppercase_at(
unbiased_index(next_u32, uppercase_count(exclude_ambiguous)),
exclude_ambiguous,
)
cursor = cursor + 1
}
if digits {
output[cursor] = digit_at(
unbiased_index(next_u32, digit_count(exclude_ambiguous)),
exclude_ambiguous,
)
cursor = cursor + 1
}
if symbols {
output[cursor] = symbol_at(
unbiased_index(next_u32, symbol_count(safe_symbols_only)),
safe_symbols_only,
)
cursor = cursor + 1
}
let pool_size = pool_size(
lowercase, uppercase, digits, symbols, exclude_ambiguous, safe_symbols_only,
)
while cursor < length {
output[cursor] = pool_at(
unbiased_index(next_u32, pool_size),
lowercase,
uppercase,
digits,
symbols,
exclude_ambiguous,
safe_symbols_only,
)
cursor = cursor + 1
}
let mut index = length - 1
while index > 0 {
let other = unbiased_index(next_u32, index + 1)
let temporary = output[index]
output[index] = output[other]
output[other] = temporary
index = index - 1
}
if !candidate_valid(
output, lowercase, uppercase, digits, symbols, exclude_ambiguous, safe_symbols_only,
) {
return -1
}
index = 0
while index < length {
if !emit(output[index]) {
return -2
}
index = index + 1
}
0
}
///|
/// Generate a decimal PIN into a caller-owned sink.
pub fn generate_pin_into(
next_u32 : () -> UInt,
emit : (Int) -> Bool,
length : Int,
) -> Int {
if length < 4 || length > 32 {
return -1
}
let mut index = 0
while index < length {
if !emit(48 + unbiased_index(next_u32, 10)) {
return -2
}
index = index + 1
}
0
}
///|
/// Generate a decimal PIN as a MoonBit `String`.
pub fn generate_pin(
length : Int,
next_u32 : () -> UInt,
) -> Result[String, String] {
let builder = StringBuilder()
let result = generate_pin_into(
next_u32,
fn(code) {
match code.to_char() {
Some(ch) => {
builder.write_char(ch)
true
}
None => false
}
},
length,
)
match result {
0 => Ok(builder.to_string())
-1 => Err("PIN length must be between 4 and 32")
_ => Err("output rejected generated digit")
}
}
///|
/// Generate a passphrase into a caller-owned sink. Dictionary access remains
/// callback-based so callers can use arrays, memory-mapped data, or Flash.
pub fn generate_passphrase_into(
next_u32 : () -> UInt,
emit : (Int) -> Bool,
dictionary_count : Int,
dictionary_length : (Int) -> Int,
dictionary_char : (Int, Int) -> Int,
word_count : Int,
capitalize : Bool,
complete_word : Bool,
separator_index : Int,
) -> Int {
let separator = separator_at(separator_index)
if word_count < 3 || word_count > 6 || dictionary_count <= 0 || separator < 0 {
return -1
}
let mut word_number = 0
while word_number < word_count {
let word_index = unbiased_index(next_u32, dictionary_count)
let source_length = dictionary_length(word_index)
if source_length <= 0 {
return -1
}
let output_length = if complete_word || source_length <= 4 {
source_length
} else {
4
}
let mut character_index = 0
while character_index < output_length {
let mut ch = dictionary_char(word_index, character_index)
if !is_lower(ch) {
return -1
}
if capitalize && character_index == 0 {
ch = ch - 32
}
if !emit(ch) {
return -2
}
character_index = character_index + 1
}
if word_number + 1 < word_count && !emit(separator) {
return -2
}
word_number = word_number + 1
}
0
}
///|
/// Map a uniform 32-bit sample to `[0, bound)` without modulo bias.
pub fn unbiased_index(next_u32 : () -> UInt, bound : Int) -> Int {
if bound <= 0 {
return -1
}
if bound == 1 {
return 0
}
let divisor = bound.reinterpret_as_uint()
let limit = 0xffffffffU - 0xffffffffU % divisor
let mut sample = next_u32()
while sample >= limit {
sample = next_u32()
}
(sample % divisor).reinterpret_as_int()
}
///|
fn policy_valid(
length : Int,
lowercase : Bool,
uppercase : Bool,
digits : Bool,
symbols : Bool,
) -> Bool {
let required = (if lowercase { 1 } else { 0 }) +
(if uppercase { 1 } else { 0 }) +
(if digits { 1 } else { 0 }) +
(if symbols { 1 } else { 0 })
length >= 4 && length <= 128 && required > 0 && length >= required
}
///|
fn is_lower(ch : Int) -> Bool {
ch >= 97 && ch <= 122
}
///|
fn is_upper(ch : Int) -> Bool {
ch >= 65 && ch <= 90
}
///|
fn is_digit(ch : Int) -> Bool {
ch >= 48 && ch <= 57
}
///|
fn is_ambiguous(ch : Int) -> Bool {
ch == 48 || ch == 79 || ch == 49 || ch == 73 || ch == 108
}
///|
fn is_symbol(ch : Int) -> Bool {
ch == 33 ||
ch == 64 ||
ch == 35 ||
ch == 36 ||
ch == 37 ||
ch == 94 ||
ch == 38 ||
ch == 42 ||
ch == 95 ||
ch == 45 ||
ch == 43 ||
ch == 61 ||
ch == 63
}
///|
fn is_safe_symbol(ch : Int) -> Bool {
ch == 33 ||
ch == 64 ||
ch == 35 ||
ch == 36 ||
ch == 37 ||
ch == 42 ||
ch == 95 ||
ch == 45 ||
ch == 63
}
///|
fn lowercase_count(exclude_ambiguous : Bool) -> Int {
if exclude_ambiguous {
25
} else {
26
}
}
///|
fn uppercase_count(exclude_ambiguous : Bool) -> Int {
if exclude_ambiguous {
24
} else {
26
}
}
///|
fn digit_count(exclude_ambiguous : Bool) -> Int {
if exclude_ambiguous {
8
} else {
10
}
}
///|
fn lowercase_at(index : Int, exclude_ambiguous : Bool) -> Int {
if exclude_ambiguous && index >= 11 {
98 + index
} else {
97 + index
}
}
///|
fn uppercase_at(index : Int, exclude_ambiguous : Bool) -> Int {
if !exclude_ambiguous {
return 65 + index
}
if index < 8 {
65 + index
} else if index < 13 {
66 + index
} else {
67 + index
}
}
///|
fn digit_at(index : Int, exclude_ambiguous : Bool) -> Int {
if exclude_ambiguous {
50 + index
} else {
48 + index
}
}
///|
fn symbol_count(safe_only : Bool) -> Int {
if safe_only {
9
} else {
13
}
}
///|
fn symbol_at(index : Int, safe_only : Bool) -> Int {
if safe_only {
match index {
0 => 33
1 => 64
2 => 35
3 => 36
4 => 37
5 => 42
6 => 95
7 => 45
8 => 63
_ => -1
}
} else {
match index {
0 => 33
1 => 64
2 => 35
3 => 36
4 => 37
5 => 94
6 => 38
7 => 42
8 => 95
9 => 45
10 => 43
11 => 61
12 => 63
_ => -1
}
}
}
///|
fn pool_size(
lowercase : Bool,
uppercase : Bool,
digits : Bool,
symbols : Bool,
exclude_ambiguous : Bool,
safe_symbols_only : Bool,
) -> Int {
(if lowercase { lowercase_count(exclude_ambiguous) } else { 0 }) +
(if uppercase { uppercase_count(exclude_ambiguous) } else { 0 }) +
(if digits { digit_count(exclude_ambiguous) } else { 0 }) +
(if symbols { symbol_count(safe_symbols_only) } else { 0 })
}
///|
fn pool_at(
original_index : Int,
lowercase : Bool,
uppercase : Bool,
digits : Bool,
_symbols : Bool,
exclude_ambiguous : Bool,
safe_symbols_only : Bool,
) -> Int {
let mut index = original_index
if lowercase {
let count = lowercase_count(exclude_ambiguous)
if index < count {
return lowercase_at(index, exclude_ambiguous)
}
index = index - count
}
if uppercase {
let count = uppercase_count(exclude_ambiguous)
if index < count {
return uppercase_at(index, exclude_ambiguous)
}
index = index - count
}
if digits {
let count = digit_count(exclude_ambiguous)
if index < count {
return digit_at(index, exclude_ambiguous)
}
index = index - count
}
symbol_at(index, safe_symbols_only)
}
///|
fn candidate_valid(
output : FixedArray[Int],
lowercase : Bool,
uppercase : Bool,
digits : Bool,
symbols : Bool,
exclude_ambiguous : Bool,
safe_symbols_only : Bool,
) -> Bool {
let mut has_lower = false
let mut has_upper = false
let mut has_digit = false
let mut has_symbol = false
for ch in output {
if exclude_ambiguous && is_ambiguous(ch) {
return false
}
if is_lower(ch) {
if !lowercase {
return false
}
has_lower = true
} else if is_upper(ch) {
if !uppercase {
return false
}
has_upper = true
} else if is_digit(ch) {
if !digits {
return false
}
has_digit = true
} else if is_symbol(ch) {
if !symbols || (safe_symbols_only && !is_safe_symbol(ch)) {
return false
}
has_symbol = true
} else {
return false
}
}
(!lowercase || has_lower) &&
(!uppercase || has_upper) &&
(!digits || has_digit) &&
(!symbols || has_symbol)
}
///|
fn separator_at(index : Int) -> Int {
match index {
0 => 45
1 => 46
2 => 95
_ => -1
}
}