diff --git a/akon-core/src/auth/keyring.rs b/akon-core/src/auth/keyring.rs index 4f6a269..07fd4f1 100644 --- a/akon-core/src/auth/keyring.rs +++ b/akon-core/src/auth/keyring.rs @@ -77,7 +77,18 @@ pub fn retrieve_pin(username: &str) -> Result { .get_password() .map_err(|_| AkonError::Keyring(KeyringError::PinNotFound))?; - Pin::new(pin_str).map_err(AkonError::Otp) + // Enforce the internal hard limit of 30 characters at retrieval time. + // This truncation is silent and ensures downstream consumers never see + // a PIN longer than 30 characters. + let pin_trimmed = pin_str.trim().to_string(); + let stored = if pin_trimmed.chars().count() > 30 { + pin_trimmed.chars().take(30).collect::() + } else { + pin_trimmed.clone() + }; + + // Return a Pin without re-applying strict 4-digit validation. + Ok(Pin::from_unchecked(stored)) } /// Check if a PIN exists in the keyring for the given username diff --git a/akon-core/src/types.rs b/akon-core/src/types.rs index ba378dc..3e752fb 100644 --- a/akon-core/src/types.rs +++ b/akon-core/src/types.rs @@ -102,6 +102,14 @@ impl Pin { Ok(Self(Secret::new(pin))) } + /// Create a PIN without enforcing the 4-digit numeric validation. + /// + /// This is provided for cases where the user prefers a different PIN format + /// (e.g., longer PINs or alphanumeric PINs). Use with caution. + pub fn from_unchecked(pin: String) -> Self { + Self(Secret::new(pin)) + } + /// Expose the PIN value (use with caution!) /// /// This should only be called when absolutely necessary, diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 3d664da..bb81d19 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -6,7 +6,7 @@ use akon_core::{ auth::keyring, config::{toml_config, VpnConfig}, error::AkonError, - types::OtpSecret, + types::{OtpSecret, Pin}, }; use colored::Colorize; use std::io::{self, Write}; @@ -54,6 +54,8 @@ pub fn run_setup() -> Result<(), AkonError> { // Collect configuration interactively let config = collect_vpn_config()?; let otp_secret = collect_otp_secret()?; + let pin = collect_pin()?; + let reconnection_policy = collect_reconnection_config()?; // Validate configuration @@ -77,7 +79,8 @@ pub fn run_setup() -> Result<(), AkonError> { // Save config to TOML file with reconnection policy toml_config::save_config_with_reconnection(&config, reconnection_policy.as_ref())?; - // Store OTP secret in keyring + // Store PIN and OTP secret in keyring + keyring::store_pin(&config.username, &pin)?; keyring::store_otp_secret(&config.username, otp_secret.expose())?; println!( @@ -335,6 +338,38 @@ fn collect_otp_secret() -> Result { } } +/// Collect 4-digit PIN interactively +fn collect_pin() -> Result { + println!(); + println!("PIN Configuration:"); + println!("-----------------"); + + println!( + "Enter your VPN PIN (any format). This will be stored securely in your system keyring." + ); + println!(); + + loop { + let pin_str = prompt_password("PIN")?; + let candidate = pin_str.trim().to_string(); + + if candidate.is_empty() { + println!("❌ PIN cannot be empty. Please try again."); + continue; + } + + // Enforce a hard internal limit of 30 characters for stored PINs. + // This truncation is silent (hidden from the user) per request. + let stored = if candidate.chars().count() > 30 { + candidate.chars().take(30).collect::() + } else { + candidate.clone() + }; + + return Ok(Pin::from_unchecked(stored)); + } +} + /// Prompt for a required value with default fn prompt_required(prompt: &str, default: &str) -> Result { let prompt_text = if default.is_empty() {