Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion akon-core/src/auth/keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,18 @@ pub fn retrieve_pin(username: &str) -> Result<Pin, AkonError> {
.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::<String>()
} 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
Expand Down
8 changes: 8 additions & 0 deletions akon-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 37 additions & 2 deletions src/cli/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -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!(
Expand Down Expand Up @@ -335,6 +338,38 @@ fn collect_otp_secret() -> Result<OtpSecret, AkonError> {
}
}

/// Collect 4-digit PIN interactively
fn collect_pin() -> Result<Pin, AkonError> {
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::<String>()
} else {
candidate.clone()
};

return Ok(Pin::from_unchecked(stored));
}
}

/// Prompt for a required value with default
fn prompt_required(prompt: &str, default: &str) -> Result<String, AkonError> {
let prompt_text = if default.is_empty() {
Expand Down