Add Proton mailbox password helper

ober

e97c1c32ff600ec5381242d5508d890e559359f6

diff --git a/README.md b/README.md
index 8ec395d..bc517b1 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,10 @@ The read-only `folders`, `list`, and `message-json` commands perform a fresh
 interactive auth for each invocation. They fetch Proton API JSON directly and
 do not store a reusable refresh token or local IMAP password.
 
+The native helper also derives Proton's salted mailbox key passphrase from
+`/core/v4/keys/salts`; this is the key-unlock input required before decrypted
+message rendering can be enabled.
+
 `auth-info.json` is the `/auth/v4/info` response. The command prompts for the
 Proton password and emits the SRP `/auth/v4` request body plus the expected
 server proof to verify after Proton responds.
diff --git a/plan.md b/plan.md
index b02e636..609b225 100644
--- a/plan.md
+++ b/plan.md
@@ -272,6 +272,14 @@ Deliverables:
 - Unlock user keys.
 - Unlock address keys.
 
+Done:
+
+- Native mailbox key password derivation using Proton's MIT-licensed
+  `proton-srp` implementation and the same 31-byte suffix used by
+  `go-proton-api`.
+- Jerboa wrapper `proton-mailbox-password` with a deterministic Proton test
+  vector.
+
 Exit criteria:
 
 - Key unlock succeeds.
diff --git a/proton-bridge-native/Cargo.lock b/proton-bridge-native/Cargo.lock
index 0691911..dcedc94 100644
--- a/proton-bridge-native/Cargo.lock
+++ b/proton-bridge-native/Cargo.lock
@@ -1208,6 +1208,7 @@ dependencies = [
 name = "proton-bridge-native"
 version = "0.1.0"
 dependencies = [
+ "base64",
  "proton-srp",
  "zeroize",
 ]
diff --git a/proton-bridge-native/Cargo.toml b/proton-bridge-native/Cargo.toml
index 653cae8..a9a0ac8 100644
--- a/proton-bridge-native/Cargo.toml
+++ b/proton-bridge-native/Cargo.toml
@@ -9,5 +9,6 @@ name = "proton_bridge_native"
 crate-type = ["cdylib", "rlib"]
 
 [dependencies]
+base64 = "0.22"
 proton-srp = "0.8.2"
 zeroize = "1.8"
diff --git a/proton-bridge-native/src/lib.rs b/proton-bridge-native/src/lib.rs
index ea9711d..2c12f40 100644
--- a/proton-bridge-native/src/lib.rs
+++ b/proton-bridge-native/src/lib.rs
@@ -1,4 +1,5 @@
-use proton_srp::{SRPAuth, SRPProofB64, SrpHashVersion};
+use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
+use proton_srp::{mailbox_password_hash, SRPAuth, SRPProofB64, SrpHashVersion};
 use std::ffi::CStr;
 use std::os::raw::c_char;
 use std::slice;
@@ -23,6 +24,18 @@ fn set_last_error(msg: impl Into<String>) {
     }
 }
 
+fn mailbox_password_tail(password: &str, key_salt_b64: &str) -> Result<String, String> {
+    let key_salt = BASE64_STANDARD
+        .decode(key_salt_b64)
+        .map_err(|err| err.to_string())?;
+    let hashed = mailbox_password_hash(password, &key_salt).map_err(|err| err.to_string())?;
+    let hashed = std::str::from_utf8(hashed.as_bytes()).map_err(|err| err.to_string())?;
+    if hashed.len() < 31 {
+        return Err("mailbox password hash is shorter than expected".to_owned());
+    }
+    Ok(hashed[hashed.len() - 31..].to_owned())
+}
+
 unsafe fn cstr_arg(name: &str, value: *const c_char) -> Result<String, i32> {
     if value.is_null() {
         set_last_error(format!("{name} is null"));
@@ -133,6 +146,48 @@ pub unsafe extern "C" fn pb_native_srp_last_error(out: *mut u8, out_len: *mut u3
     }
 }
 
+/// Derive the Proton salted mailbox key password for a key salt.
+///
+/// Proton's API exposes per-key salts from `/core/v4/keys/salts`. The
+/// official Go client hashes the login/mailbox password with that salt and
+/// uses the final 31 bytes as the OpenPGP key passphrase.
+#[no_mangle]
+pub unsafe extern "C" fn pb_native_mailbox_password(
+    password: *const c_char,
+    key_salt_b64: *const c_char,
+    out: *mut u8,
+    out_len: *mut u32,
+) -> i32 {
+    set_last_error("");
+
+    let mut password = match cstr_arg("password", password) {
+        Ok(value) => value,
+        Err(code) => return code,
+    };
+    let key_salt_b64 = match cstr_arg("key_salt_b64", key_salt_b64) {
+        Ok(value) => value,
+        Err(code) => {
+            password.zeroize();
+            return code;
+        }
+    };
+
+    let key_pass = match mailbox_password_tail(&password, &key_salt_b64) {
+        Ok(value) => value,
+        Err(err) => {
+            password.zeroize();
+            set_last_error(err);
+            return PB_SRP_ERROR;
+        }
+    };
+    password.zeroize();
+
+    match write_string(&key_pass, out, out_len) {
+        Ok(()) => PB_SRP_OK,
+        Err(code) => code,
+    }
+}
+
 /// Generate Proton SRP authentication proofs from `/auth/v4/info` values.
 ///
 /// All string inputs must be NUL-terminated UTF-8. `salt`,
@@ -243,3 +298,15 @@ pub unsafe extern "C" fn pb_native_srp_proofs(
         out_expected_server_proof_len,
     )
 }
+
+#[cfg(test)]
+mod tests {
+    use super::mailbox_password_tail;
+
+    #[test]
+    fn derives_proton_mailbox_key_password_suffix() {
+        let key_pass = mailbox_password_tail("password", "imK9IHsRcA2Zsv+yROZgbw==").unwrap();
+        assert_eq!(key_pass, "Q.Gd9rSsqE0xQ8Qcf0Q9ckInb4hIzOu");
+        assert_eq!(key_pass.len(), 31);
+    }
+}
diff --git a/proton-bridge/srp.ss b/proton-bridge/srp.ss
index 12361ad..97e528a 100644
--- a/proton-bridge/srp.ss
+++ b/proton-bridge/srp.ss
@@ -13,7 +13,8 @@
     proton-srp-generate-proofs
     proton-srp-auth-request-payload
     proton-srp-auth-request-json
-    proton-srp-server-proof-valid?)
+    proton-srp-server-proof-valid?
+    proton-mailbox-password)
 
   (import (except (chezscheme)
                   make-hash-table hash-table?
@@ -78,11 +79,19 @@
               u8* u8* u8* u8* u8* u8*)
              integer-32))))
 
+  (define c-mailbox-password
+    (and *native-loaded?*
+         (guard (e [#t #f])
+           (foreign-procedure "pb_native_mailbox_password"
+             (string string u8* u8*)
+             integer-32))))
+
   (def (proton-srp-native-available?)
     (and *native-loaded?*
          c-srp-available
          c-srp-last-error
          c-srp-proofs
+         c-mailbox-password
          (= (c-srp-available) 1)))
 
   (def (u32-le-ref bv off)
@@ -160,6 +169,13 @@
                 proof-out (u32-le-ref proof-len 0)
                 server-out (u32-le-ref server-len 0)))))
 
+  (def (call-native-mailbox-password password key-salt-b64 cap)
+    (let* ([out (make-bytevector cap 0)]
+           [len (make-bytevector 4 0)])
+      (u32-le-set! len 0 cap)
+      (let ([rc (c-mailbox-password password key-salt-b64 out len)])
+        (values rc out (u32-le-ref len 0)))))
+
   (def (proton-srp-generate-proofs version username password salt modulus server-ephemeral)
     (check-native 'proton-srp-generate-proofs)
     (unless (and (integer? version) (>= version 0))
@@ -197,6 +213,31 @@
           [else
            (error 'proton-srp-generate-proofs (proton-srp-error-string rc))]))))
 
+  (def (proton-mailbox-password password key-salt-b64)
+    (check-native 'proton-mailbox-password)
+    (for-each
+      (lambda (name value)
+        (unless (string? value)
+          (error 'proton-mailbox-password
+                 (string-append name " must be a string"))))
+      '("password" "key-salt-b64")
+      (list password key-salt-b64))
+    (call-with-values
+      (lambda () (call-native-mailbox-password password key-salt-b64 64))
+      (lambda (rc out len)
+        (cond
+          [(= rc PB-SRP-OK)
+           (utf8->string (bv-slice out 0 len))]
+          [(= rc PB-SRP-INSUFFICIENT-BUFFER)
+           (call-with-values
+             (lambda () (call-native-mailbox-password password key-salt-b64 len))
+             (lambda (rc2 out2 len2)
+               (unless (= rc2 PB-SRP-OK)
+                 (error 'proton-mailbox-password (proton-srp-error-string rc2)))
+               (utf8->string (bv-slice out2 0 len2))))]
+          [else
+           (error 'proton-mailbox-password (proton-srp-error-string rc))]))))
+
   (def (jref who obj key)
     (unless (hash-table? obj)
       (error who "expected JSON object while reading" key))
diff --git a/test/test-all.ss b/test/test-all.ss
index 01044b4..fb999c1 100644
--- a/test/test-all.ss
+++ b/test/test-all.ss
@@ -175,6 +175,12 @@
                   (= (string-length (hashtable-ref payload "ClientEphemeral" "")) 344)
                   (= (string-length (hashtable-ref payload "ClientProof" "")) 344))))))
 
+(check "mailbox password derives Proton key passphrase suffix"
+       (let ([key-pass
+              (proton-mailbox-password "password" "imK9IHsRcA2Zsv+yROZgbw==")])
+         (and (string=? key-pass "Q.Gd9rSsqE0xQ8Qcf0Q9ckInb4hIzOu")
+              (= (string-length key-pass) 31))))
+
 (define sample-auth-after-srp
   (string->json-object
     "{\"UID\":\"uid-1\",\"AccessToken\":\"access-1\",\"RefreshToken\":\"refresh-1\",\"ServerProof\":\"proof\",\"2FA\":{\"Enabled\":2,\"FIDO2\":{\"AuthenticationOptions\":{\"publicKey\":{\"rpId\":\"proton.me\",\"challenge\":[1],\"allowCredentials\":[{\"id\":[2]}]}}}}}"))