Implement Rust-owned Android local and HTTPS onboarding (#95)
Android ARM verification / rust (push) Failing after 46m9s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 21s

This commit is contained in:
hugo committed 2026-10-09 11:31:50 +02:00
1 parent 66b1d4a4c4
commit 36e5f55283
46 files changed
+3002 -79

No files matched your search

+25
View File
@@ -266,6 +266,31 @@ impl KeyStore {
Ok(store)
}
/// Merge prepared key material while retaining existing private keys.
pub(crate) fn extend_from_path(&mut self, path: &Path) -> Result<(), CryptoError> {
let loaded = match Self::load(path) {
Ok(loaded) => loaded,
Err(CryptoError::NoKeyMaterial) => return Ok(()),
Err(error) => return Err(error),
};
if self.keys.len() + loaded.keys.len() > MAX_IMPORTED_KEYS {
return Err(CryptoError::TooManyKeys);
}
for (fingerprint, material) in loaded.keys {
if let Some(existing) = self.keys.get_mut(&fingerprint) {
if existing.public.primary_key != material.public.primary_key {
return Err(CryptoError::DuplicateFingerprint { fingerprint });
}
if existing.secret.is_none() {
existing.secret = material.secret;
}
} else {
self.keys.insert(fingerprint, material);
}
}
Ok(())
}
/// Transactionally import one armored or binary stream containing public and/or secret keys.
pub fn import(&mut self, bytes: &[u8]) -> Result<Vec<KeyInfo>, CryptoError> {
if bytes.len() as u64 > MAX_KEY_FILE_BYTES {
+44 -5
View File
@@ -1466,6 +1466,50 @@ impl GitRepository {
credentials: &impl GitRemoteCredentialProvider,
transport: &impl GitFetchTransport,
control: &GitOperationControl,
) -> Result<Vec<String>, GitError> {
let branches = Self::probe_remote_with_transport(
parent,
identity,
configured,
credentials,
transport,
control,
)?;
if branches.is_empty() {
return Err(GitError::InvalidRepository(
"the remote has no branches".to_owned(),
));
}
Ok(branches)
}
/// Validate HTTPS authentication before saving onboarding state, including an empty remote.
pub(crate) fn validate_remote_with_transport(
parent: &Path,
identity: GitIdentity,
configured: &GitRemote,
credentials: &impl GitRemoteCredentialProvider,
transport: &impl GitFetchTransport,
control: &GitOperationControl,
) -> Result<(), GitError> {
Self::probe_remote_with_transport(
parent,
identity,
configured,
credentials,
transport,
control,
)
.map(|_| ())
}
fn probe_remote_with_transport(
parent: &Path,
identity: GitIdentity,
configured: &GitRemote,
credentials: &impl GitRemoteCredentialProvider,
transport: &impl GitFetchTransport,
control: &GitOperationControl,
) -> Result<Vec<String>, GitError> {
control.report(GitProgressPhase::Validating)?;
ensure_remote_transport_available(configured.endpoint())?;
@@ -1648,11 +1692,6 @@ impl GitRepository {
.collect::<Result<Vec<_>, GitError>>()?;
branches.sort();
branches.dedup();
if branches.is_empty() {
return Err(GitError::InvalidRepository(
"the remote has no branches".to_owned(),
));
}
Ok(branches)
}
+2
View File
@@ -1,5 +1,7 @@
//! Storage-owned first-run setup for the native mobile applications.
pub mod android;
use std::{
error::Error,
fmt, fs,
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -532,7 +532,7 @@ fn regular_bytes(
}
Ok(Some(bytes))
}
fn exclusive_lock(parent: &Dir, name: &str) -> Result<fs::File, PrivateStorageError> {
pub(crate) fn exclusive_lock(parent: &Dir, name: &str) -> Result<fs::File, PrivateStorageError> {
if let Ok(metadata) = parent.symlink_metadata(name)
&& (metadata.file_type().is_symlink() || !metadata.is_file())
{
+38 -2
View File
@@ -1010,6 +1010,32 @@ struct SecretRecord {
fn encode_record(
reference: &SecretReference,
value: &SecretBytes,
) -> Result<SecretBytes, SecretStoreError> {
encode_record_with_owner(reference, value, None)
}
/// A unique protected owner distinguishes newly created credentials during crash rollback.
pub(crate) fn encode_onboarding_record(
reference: &SecretReference,
value: &SecretBytes,
owner: &[u8],
) -> Result<SecretBytes, SecretStoreError> {
if owner.is_empty()
|| owner.len() > 64
|| !matches!(
reference.kind,
SecretReferenceKind::HttpsGitCredential { .. }
)
{
return Err(SecretStoreError::InvalidReference);
}
encode_record_with_owner(reference, value, Some(owner))
}
fn encode_record_with_owner(
reference: &SecretReference,
value: &SecretBytes,
owner: Option<&[u8]>,
) -> Result<SecretBytes, SecretStoreError> {
validate_secret(value)?;
let mut encoded = Vec::with_capacity(RECORD_MAGIC.len() + value.expose().len() + 1024);
@@ -1025,7 +1051,10 @@ fn encode_record(
application_id,
account,
} => {
encoded.push(2);
encoded.push(if owner.is_some() { 4 } else { 2 });
if let Some(owner) = owner {
write_field(&mut encoded, owner)?;
}
write_field(&mut encoded, server_id.as_bytes())?;
write_field(&mut encoded, application_id.as_bytes())?;
write_field(&mut encoded, account.as_bytes())?;
@@ -1063,7 +1092,14 @@ fn decode_record(encoded: SecretBytes) -> Result<SecretRecord, SecretStoreError>
SecretReference::openpgp_passphrase(read_text(fingerprint)?)
.map_err(|_| SecretStoreError::Corrupted)?
}
2 => {
2 | 4 => {
if kind == 4 {
let (owner, rest) = read_field(remainder)?;
if owner.is_empty() || owner.len() > 64 {
return Err(SecretStoreError::Corrupted);
}
remainder = rest;
}
let (server_id, rest) = read_field(remainder)?;
let (application_id, rest) = read_field(rest)?;
let (account, rest) = read_field(rest)?;