openadp_ocrypt/
ocrypt.rs

1//! Ocrypt - Drop-in replacement for password hashing functions
2//!
3//! Ocrypt provides a simple 3-function API that replaces traditional password hashing functions
4//! (bcrypt, scrypt, Argon2, PBKDF2) with OpenADP's distributed threshold cryptography for
5//! nation-state-resistant password protection.
6
7use crate::{OpenADPError, Result};
8use crate::keygen::{generate_encryption_key, recover_encryption_key};
9use crate::client::{ServerInfo, get_servers};
10use serde::{Deserialize, Serialize};
11use aes_gcm::{Aes256Gcm, Key, Nonce, KeyInit};
12use aes_gcm::aead::Aead;
13use rand::Rng;
14use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
15use sha2::{Sha256, Digest};
16use hex;
17
18
19/// Wrapped secret data
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct WrappedSecret {
22    pub nonce: String,
23    pub ciphertext: String,
24    pub tag: String,
25}
26
27/// Ocrypt metadata format
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct OcryptMetadata {
30    // Standard openadp-encrypt fields
31    pub servers: Vec<String>,
32    pub threshold: usize,
33    pub version: String,
34    pub auth_code: String,
35    pub user_id: String,
36    
37    // Ocrypt-specific fields
38    pub wrapped_long_term_secret: WrappedSecret,
39    pub backup_id: String,
40    pub app_id: String,
41    pub max_guesses: i32,
42    pub ocrypt_version: String,
43}
44
45/// Register a long-term secret protected by a PIN using OpenADP distributed cryptography.
46///
47/// This function provides a simple interface that replaces traditional password hashing
48/// functions like bcrypt, scrypt, Argon2, and PBKDF2 with distributed threshold cryptography.
49///
50/// # Arguments
51///
52/// * `user_id` - Unique identifier for the user (e.g., email, username)
53/// * `app_id` - Application identifier to namespace secrets per app
54/// * `long_term_secret` - User-provided secret to protect (any byte sequence)
55/// * `pin` - Password/PIN that will unlock the secret
56/// * `max_guesses` - Maximum wrong attempts before lockout
57/// * `servers_url` - Optional custom URL for server registry (empty string uses default)
58///
59/// # Returns
60///
61/// Returns metadata bytes that should be stored alongside the user record.
62///
63/// # Example
64///
65/// ```rust,no_run
66/// use openadp_ocrypt::register;
67///
68/// #[tokio::main]
69/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
70///     let secret = b"my_api_key_or_private_key";
71///     let metadata = register(
72///         "alice@example.com",
73///         "document_signing",
74///         secret,
75///         "secure_pin_123",
76///         10,
77///         "",
78///     ).await?;
79///     
80///     // Store metadata with user record
81///     println!("Metadata length: {} bytes", metadata.len());
82///     Ok(())
83/// }
84/// ```
85pub async fn register(
86    user_id: &str,
87    app_id: &str,
88    long_term_secret: &[u8],
89    pin: &str,
90    max_guesses: i32,
91    servers_url: &str,
92) -> Result<Vec<u8>> {
93    register_with_bid(user_id, app_id, long_term_secret, pin, max_guesses, "even", servers_url).await
94}
95
96/// Recover a long-term secret using the PIN with automatic backup refresh.
97///
98/// This function implements a two-phase commit pattern for safe backup refresh:
99/// 1. Recovers the secret using existing backup
100/// 2. Attempts to refresh backup with opposite backup ID
101/// 3. Returns updated metadata if refresh succeeds, original if it fails
102///
103/// # Arguments
104///
105/// * `metadata_bytes` - Metadata blob from register()
106/// * `pin` - Password/PIN to unlock the secret
107/// * `servers_url` - Optional custom URL for server registry (empty string uses default)
108///
109/// # Returns
110///
111/// Returns a tuple of (secret, remaining_guesses, updated_metadata).
112///
113/// # Example
114///
115/// ```rust,no_run
116/// use openadp_ocrypt::{register, recover};
117///
118/// #[tokio::main]
119/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
120///     // ... register first ...
121///     let metadata = register("alice@example.com", "app", b"secret", "pin", 10, "").await?;
122///     
123///     // Later: recover with automatic backup refresh
124///     let (secret, remaining, updated_metadata) = recover(&metadata, "pin", "").await?;
125///     
126///     // Store updated_metadata if it changed
127///     if updated_metadata != metadata {
128///         println!("Backup was refreshed, store updated metadata");
129///     }
130///     
131///     Ok(())
132/// }
133/// ```
134pub async fn recover(
135    metadata_bytes: &[u8],
136    pin: &str,
137    servers_url: &str,
138) -> Result<(Vec<u8>, u32, Vec<u8>)> {
139    // Step 1: Recover secret using existing backup
140    eprintln!("📋 Step 1: Recovering with existing backup...");
141    let (secret, remaining) = recover_without_refresh(metadata_bytes, pin, servers_url).await?;
142
143    // Step 2: Attempt automatic backup refresh with two-phase commit
144    let updated_metadata = match attempt_backup_refresh(&secret, metadata_bytes, pin, servers_url).await {
145        Ok(new_metadata) => {
146            eprintln!("✅ Backup refresh successful");
147            new_metadata
148        }
149        Err(e) => {
150            eprintln!("⚠️  Backup refresh failed (using original): {}", e);
151            metadata_bytes.to_vec()
152        }
153    };
154
155    Ok((secret, remaining, updated_metadata))
156}
157
158/// Recover a long-term secret and reregister with completely fresh metadata.
159///
160/// This function provides a clean separation between recovery and registration:
161/// 1. Recovers the long-term secret using existing metadata
162/// 2. Performs a completely fresh registration with new cryptographic material
163///
164/// # Arguments
165///
166/// * `metadata_bytes` - Metadata blob from register()
167/// * `pin` - Password/PIN to unlock the secret  
168/// * `servers_url` - Optional custom URL for server registry (empty string uses default)
169///
170/// # Returns
171///
172/// Returns a tuple of (secret, new_metadata).
173///
174/// # Example
175///
176/// ```rust,no_run
177/// use openadp_ocrypt::{register, recover_and_reregister};
178///
179/// #[tokio::main]
180/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
181///     // ... register first ...
182///     let metadata = register("alice@example.com", "app", b"secret", "pin", 10, "").await?;
183///     
184///     // Later: recover and get completely fresh metadata
185///     let (secret, new_metadata) = recover_and_reregister(&metadata, "pin", "").await?;
186///     
187///     // new_metadata contains completely fresh cryptographic material
188///     println!("Fresh metadata length: {} bytes", new_metadata.len());
189///     
190///     Ok(())
191/// }
192/// ```
193pub async fn recover_and_reregister(
194    metadata_bytes: &[u8],
195    pin: &str,
196    servers_url: &str,
197) -> Result<(Vec<u8>, Vec<u8>)> {
198    // Input validation
199    if metadata_bytes.is_empty() {
200        return Err(OpenADPError::InvalidInput("metadata cannot be empty".to_string()));
201    }
202    if pin.is_empty() {
203        return Err(OpenADPError::InvalidInput("pin cannot be empty".to_string()));
204    }
205
206    eprintln!("🔄 Starting recovery and re-registration...");
207
208    // Step 1: Recover with existing metadata (without refresh)
209    eprintln!("📋 Step 1: Recovering with existing metadata...");
210    let (secret, remaining) = recover_without_refresh(metadata_bytes, pin, servers_url).await?;
211    
212    // Parse original metadata to get registration parameters
213    let metadata_text = String::from_utf8_lossy(metadata_bytes);
214    let metadata: OcryptMetadata = serde_json::from_str(&metadata_text)
215        .map_err(|e| OpenADPError::InvalidInput(format!("Invalid metadata format: {}", e)))?;
216    
217    // Extract original registration parameters
218    let user_id = &metadata.user_id;
219    let app_id = &metadata.app_id;
220    let max_guesses = metadata.max_guesses;
221
222    eprintln!("   ✅ Secret recovered successfully ({} guesses remaining)", remaining);
223    eprintln!("   🔑 User: {}, App: {}", user_id, app_id);
224
225    // Step 2: Completely fresh registration with new cryptographic material
226    eprintln!("📋 Step 2: Fresh registration with new cryptographic material...");
227    
228    // Generate next backup ID to ensure alternation (critical for prepare/commit safety)
229    let old_backup_id = &metadata.backup_id;
230    let new_backup_id = generate_next_backup_id(old_backup_id);
231    eprintln!("🔄 Backup ID alternation: {} → {}", old_backup_id, new_backup_id);
232    
233    let new_metadata = register_with_bid(user_id, app_id, &secret, pin, max_guesses, &new_backup_id, servers_url).await?;
234
235    eprintln!("✅ Recovery and re-registration complete!");
236    eprintln!("   📝 New metadata contains completely fresh cryptographic material");
237    
238    Ok((secret, new_metadata))
239}
240
241/// Internal function to register with specific backup ID
242async fn register_with_bid(
243    user_id: &str,
244    app_id: &str,
245    long_term_secret: &[u8],
246    pin: &str,
247    max_guesses: i32,
248    backup_id: &str,
249    servers_url: &str,
250) -> Result<Vec<u8>> {
251    // Input validation
252    validate_inputs(user_id, app_id, long_term_secret, pin, max_guesses)?;
253
254    eprintln!("🔐 Protecting secret for user: {}", user_id);
255    eprintln!("📱 Application: {}", app_id);
256    eprintln!("🔑 Secret length: {} bytes", long_term_secret.len());
257
258    // Step 1: Server discovery
259    eprintln!("🌐 Discovering OpenADP servers...");
260    let server_infos = discover_servers(servers_url).await?;
261    
262    if server_infos.is_empty() {
263        return Err(OpenADPError::NoServers);
264    }
265
266    // Random server selection for load balancing
267    let selected_servers = if server_infos.len() > 15 { // MAX_SERVERS_FOR_LOAD_BALANCING
268        use rand::seq::SliceRandom;
269        let mut servers = server_infos;
270        servers.shuffle(&mut rand::thread_rng());
271        servers.into_iter().take(15).collect()
272    } else {
273        server_infos
274    };
275
276    eprintln!("📋 Using {} servers for registration", selected_servers.len());
277
278    // Step 2: Generate encryption key using OpenADP protocol
279    eprintln!("🔄 Using backup ID: {}", backup_id);
280    eprintln!("🔑 Generating encryption key using OpenADP servers...");
281
282    // Create Identity from Ocrypt parameters
283    let identity = crate::keygen::Identity::new(
284        user_id.to_string(),  // UID = userID (user identifier)
285        app_id.to_string(),   // DID = appID (application identifier, serves as device ID)
286        backup_id.to_string() // BID = backupID (managed by Ocrypt: "even"/"odd")
287    );
288    
289    let key_result = generate_encryption_key(&identity, pin, max_guesses, 0, selected_servers).await?;
290
291    if key_result.encryption_key.is_none() {
292        return Err(OpenADPError::Server(
293            key_result.error.unwrap_or_else(|| "Key generation failed".to_string())
294        ));
295    }
296
297    let encryption_key = key_result.encryption_key.unwrap();
298    let auth_codes = key_result.auth_codes.unwrap();
299    let server_infos = key_result.server_infos.unwrap();
300    let threshold = key_result.threshold.unwrap();
301
302    eprintln!("✅ Generated encryption key with {} servers", server_infos.len());
303
304    // Step 3: Wrap the long-term secret
305    eprintln!("🔐 Wrapping long-term secret...");
306    let wrapped_secret = wrap_secret(long_term_secret, &encryption_key)?;
307
308    // Step 4: Create metadata
309    let server_urls: Vec<String> = server_infos.iter().map(|s| s.url.clone()).collect();
310    
311    let metadata = OcryptMetadata {
312        servers: server_urls,
313        threshold,
314        version: "1.0".to_string(),
315        auth_code: auth_codes.base_auth_code,
316        user_id: user_id.to_string(),
317        wrapped_long_term_secret: wrapped_secret,
318        backup_id: backup_id.to_string(),
319        app_id: app_id.to_string(),
320        max_guesses,
321        ocrypt_version: "1.0".to_string(),
322    };
323
324    let metadata_bytes = serde_json::to_vec(&metadata)?;
325    eprintln!("📦 Created metadata ({} bytes)", metadata_bytes.len());
326    eprintln!("🎯 Threshold: {}-of-{} recovery", metadata.threshold, metadata.servers.len());
327
328    Ok(metadata_bytes)
329}
330
331/// Internal function to recover without backup refresh
332async fn recover_without_refresh(
333    metadata_bytes: &[u8],
334    pin: &str,
335    servers_url: &str,
336) -> Result<(Vec<u8>, u32)> {
337    // Parse metadata
338    let metadata: OcryptMetadata = serde_json::from_slice(metadata_bytes)?;
339    
340    // Recover encryption key using OpenADP protocol
341    // Create Identity from metadata
342    let identity = crate::keygen::Identity::new(
343        metadata.user_id.clone(),   // UID = userID
344        metadata.app_id.clone(),    // DID = appID  
345        metadata.backup_id.clone()  // BID = backupID
346    );
347    
348    // Get server info (use custom URL if provided, otherwise use servers from metadata)
349    let server_infos = if servers_url.is_empty() {
350        // Use servers from metadata
351        metadata.servers.iter().map(|url| ServerInfo {
352            url: url.clone(),
353            public_key: String::new(),
354            country: String::new(),
355            remaining_guesses: None,
356        }).collect()
357    } else {
358        // Use custom registry
359        discover_servers(servers_url).await?
360    };
361
362    // Create auth codes from metadata
363    let mut server_auth_codes = std::collections::HashMap::new();
364    
365    // Reconstruct server auth codes from base auth code (same logic as other implementations)
366    for server_url in &metadata.servers {
367        let combined = format!("{}:{}", metadata.auth_code, server_url);
368        let mut hasher = Sha256::new();
369        hasher.update(combined.as_bytes());
370        let hash = hasher.finalize();
371        let server_code = hex::encode(&hash);
372        server_auth_codes.insert(server_url.clone(), server_code);
373    }
374    
375    let auth_codes = crate::keygen::AuthCodes {
376        base_auth_code: metadata.auth_code.clone(),
377        server_auth_codes,
378    };
379
380    let recovery_result = recover_encryption_key(
381        &identity,
382        pin, 
383        server_infos, 
384        metadata.threshold, 
385        auth_codes
386    ).await?;
387    
388    if recovery_result.encryption_key.is_none() {
389        return Err(OpenADPError::Server(
390            recovery_result.error.unwrap_or_else(|| "Key recovery failed".to_string())
391        ));
392    }
393    
394    let encryption_key = recovery_result.encryption_key.unwrap();
395    
396    // Unwrap the long-term secret
397    let secret = unwrap_secret(&metadata.wrapped_long_term_secret, &encryption_key, recovery_result.max_guesses, recovery_result.num_guesses)?;
398    
399    Ok((secret, 0)) // 0 remaining guesses = success
400}
401
402/// Attempt to refresh backup using two-phase commit
403async fn attempt_backup_refresh(
404    secret: &[u8],
405    metadata_bytes: &[u8],
406    pin: &str,
407    servers_url: &str,
408) -> Result<Vec<u8>> {
409    let metadata: OcryptMetadata = serde_json::from_slice(metadata_bytes)?;
410    
411    // Generate next backup ID
412    let new_backup_id = generate_next_backup_id(&metadata.backup_id);
413    
414    eprintln!("🔄 Attempting backup refresh: {} → {}", metadata.backup_id, new_backup_id);
415    
416    // Phase 1: PREPARE - Register new backup (old one still exists)
417    let new_metadata = register_with_bid(
418        &metadata.user_id,
419        &metadata.app_id,
420        secret,
421        pin,
422        metadata.max_guesses,
423        &new_backup_id,
424        servers_url,
425    ).await?;
426
427    // Phase 2: COMMIT - Verify new backup works
428    let (recovered_secret, _) = recover_without_refresh(&new_metadata, pin, servers_url).await?;
429    
430    if recovered_secret == secret {
431        eprintln!("✅ Two-phase commit verification successful");
432        Ok(new_metadata)
433    } else {
434        Err(OpenADPError::Server("Two-phase commit verification failed".to_string()))
435    }
436}
437
438/// Discover servers from registry
439async fn discover_servers(servers_url: &str) -> Result<Vec<ServerInfo>> {
440    let registry_url = if servers_url.is_empty() {
441        crate::DEFAULT_REGISTRY_URL
442    } else {
443        servers_url
444    };
445
446    eprintln!("🌐 Discovering servers from registry: {}", registry_url);
447
448    let servers = get_servers(registry_url).await?;
449    
450    eprintln!("   ✅ Successfully fetched {} servers from registry", servers.len());
451    eprintln!("   📋 {} servers are live and ready", servers.len());
452    
453    Ok(servers)
454}
455
456/// Wrap secret with AES-256-GCM
457fn wrap_secret(secret: &[u8], key: &[u8]) -> Result<WrappedSecret> {
458    if key.len() != 32 {
459        return Err(OpenADPError::Crypto("Key must be 32 bytes".to_string()));
460    }
461    
462    let key = Key::<Aes256Gcm>::from_slice(key);
463    let cipher = Aes256Gcm::new(key);
464    
465    let nonce_bytes: [u8; 12] = rand::thread_rng().gen();
466    let nonce = Nonce::from_slice(&nonce_bytes);
467    
468    let ciphertext = cipher.encrypt(nonce, secret)
469        .map_err(|e| OpenADPError::Crypto(format!("AES-GCM encryption failed: {}", e)))?;
470    
471    // Split ciphertext and tag (AES-GCM appends 16-byte tag)
472    let (encrypted_data, tag) = ciphertext.split_at(ciphertext.len() - 16);
473    
474    Ok(WrappedSecret {
475        nonce: BASE64.encode(&nonce_bytes),
476        ciphertext: BASE64.encode(encrypted_data),
477        tag: BASE64.encode(tag),
478    })
479}
480
481/// Unwrap secret with AES-256-GCM
482fn unwrap_secret(wrapped: &WrappedSecret, key: &[u8], max_guesses: i32, num_guesses: i32) -> Result<Vec<u8>> {
483    if key.len() != 32 {
484        return Err(OpenADPError::Crypto("Key must be 32 bytes".to_string()));
485    }
486    
487    let key = Key::<Aes256Gcm>::from_slice(key);
488    let cipher = Aes256Gcm::new(key);
489    
490    let nonce_bytes = BASE64.decode(&wrapped.nonce)
491        .map_err(|e| OpenADPError::Crypto(format!("Invalid nonce: {}", e)))?;
492    let nonce = Nonce::from_slice(&nonce_bytes);
493    
494    let encrypted_data = BASE64.decode(&wrapped.ciphertext)
495        .map_err(|e| OpenADPError::Crypto(format!("Invalid ciphertext: {}", e)))?;
496    let tag = BASE64.decode(&wrapped.tag)
497        .map_err(|e| OpenADPError::Crypto(format!("Invalid tag: {}", e)))?;
498    
499    // Combine ciphertext and tag for AES-GCM
500    let mut ciphertext_with_tag = encrypted_data;
501    ciphertext_with_tag.extend_from_slice(&tag);
502    
503    let plaintext = cipher.decrypt(nonce, ciphertext_with_tag.as_slice())
504        .map_err(|_| {
505            // Show helpful message with actual remaining guesses
506            if max_guesses > 0 && num_guesses > 0 {
507                let remaining = max_guesses - num_guesses;
508                if remaining > 0 {
509                    eprintln!("❌ Invalid PIN! You have {} guesses remaining.", remaining);
510                } else {
511                    eprintln!("❌ Invalid PIN! No more guesses remaining - account may be locked.");
512                }
513            } else {
514                eprintln!("❌ Invalid PIN! Check your password and try again.");
515            }
516            OpenADPError::Authentication("Invalid PIN or corrupted data".to_string())
517        })?;
518    
519    Ok(plaintext)
520}
521
522/// Generate next backup ID using alternation strategy
523fn generate_next_backup_id(current_backup_id: &str) -> String {
524    match current_backup_id {
525        "even" => "odd".to_string(),
526        "odd" => "even".to_string(),
527        _ => {
528            // For versioned backup IDs, increment version
529            if current_backup_id.starts_with('v') {
530                let version_num: u32 = current_backup_id[1..].parse().unwrap_or(1);
531                format!("v{}", version_num + 1)
532            } else {
533                // Fallback to timestamped
534                use std::time::{SystemTime, UNIX_EPOCH};
535                let timestamp = SystemTime::now()
536                    .duration_since(UNIX_EPOCH)
537                    .unwrap()
538                    .as_secs();
539                format!("{}_v{}", current_backup_id, timestamp)
540            }
541        }
542    }
543}
544
545/// Validate input parameters
546fn validate_inputs(
547    user_id: &str,
548    app_id: &str,
549    long_term_secret: &[u8],
550    pin: &str,
551    max_guesses: i32,
552) -> Result<()> {
553    if user_id.is_empty() {
554        return Err(OpenADPError::InvalidInput("user_id cannot be empty".to_string()));
555    }
556    
557    if app_id.is_empty() {
558        return Err(OpenADPError::InvalidInput("app_id cannot be empty".to_string()));
559    }
560    
561    if long_term_secret.is_empty() {
562        return Err(OpenADPError::InvalidInput("long_term_secret cannot be empty".to_string()));
563    }
564    
565    if pin.is_empty() {
566        return Err(OpenADPError::InvalidInput("pin cannot be empty".to_string()));
567    }
568    
569    if max_guesses <= 0 {
570        return Err(OpenADPError::InvalidInput("max_guesses must be at least 1".to_string()));
571    }
572    
573    Ok(())
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_backup_id_generation() {
582        assert_eq!(generate_next_backup_id("even"), "odd");
583        assert_eq!(generate_next_backup_id("odd"), "even");
584        assert_eq!(generate_next_backup_id("v1"), "v2");
585        assert_eq!(generate_next_backup_id("v42"), "v43");
586        
587        let timestamped = generate_next_backup_id("production");
588        assert!(timestamped.starts_with("production_v"));
589    }
590    
591    #[test]
592    fn test_secret_wrapping() {
593        let secret = b"test_secret";
594        let key = [42u8; 32];
595        
596        let wrapped = wrap_secret(secret, &key).unwrap();
597        let unwrapped = unwrap_secret(&wrapped, &key, 10, 0).unwrap();
598        
599        assert_eq!(secret, unwrapped.as_slice());
600    }
601    
602    #[test]
603    fn test_input_validation() {
604        assert!(validate_inputs("", "app", b"secret", "pin", 10).is_err());
605        assert!(validate_inputs("user", "", b"secret", "pin", 10).is_err());
606        assert!(validate_inputs("user", "app", b"", "pin", 10).is_err());
607        assert!(validate_inputs("user", "app", b"secret", "", 10).is_err());
608        assert!(validate_inputs("user", "app", b"secret", "pin", 0).is_err());
609        assert!(validate_inputs("user", "app", b"secret", "pin", 10).is_ok());
610    }
611    
612    #[test]
613    fn test_metadata_serialization() {
614        let wrapped_secret = WrappedSecret {
615            nonce: "test_nonce".to_string(),
616            ciphertext: "test_ciphertext".to_string(),
617            tag: "test_tag".to_string(),
618        };
619        
620        let metadata = OcryptMetadata {
621            servers: vec!["server1".to_string(), "server2".to_string()],
622            threshold: 2,
623            version: "1.0".to_string(),
624            auth_code: "auth123".to_string(),
625            user_id: "user@example.com".to_string(),
626            wrapped_long_term_secret: wrapped_secret,
627            backup_id: "even".to_string(),
628            app_id: "test_app".to_string(),
629            max_guesses: 10,
630            ocrypt_version: "1.0".to_string(),
631        };
632        
633        let serialized = serde_json::to_vec(&metadata).unwrap();
634        let deserialized: OcryptMetadata = serde_json::from_slice(&serialized).unwrap();
635        
636        assert_eq!(metadata.user_id, deserialized.user_id);
637        assert_eq!(metadata.app_id, deserialized.app_id);
638        assert_eq!(metadata.threshold, deserialized.threshold);
639    }
640}