From bff18e6df4825e26e9e86b57bf17d0b5a7433f32 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Mon, 13 Jan 2025 23:39:35 -0500 Subject: [PATCH] Add proper support for Android's CA trust stores golang does not support Android's various trust stores well. The CA certificates in /system/etc/security/cacerts are the only ones that are properly read. The updatable apex trust store in Android 14+ and the user trust store are ignored. System CA certificates that the user disables are still permitted to be used. This commit implements our own certificate loading mechanism that loads from all of Android's trust stores and respects disabled certificates. We generate a combined PEM file in a temp file and feed that to rclone's CaCert option. While live reloading is supported, the user experience is not that great. Due to rclone's caching, when certificates are reloaded, they only take effect in new remotes. The user will need to delete and recreate the remote, export and reimport, or force close RSAF. Fixes: #119 Signed-off-by: Andrew Gunnerson --- .../chiller3/rsaf/rclone/RcloneProvider.kt | 20 ++ rcbridge/rcbridge.go | 175 ++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt b/app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt index 69b263d..46021e9 100644 --- a/app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt +++ b/app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt @@ -5,8 +5,11 @@ package com.chiller3.rsaf.rclone +import android.content.BroadcastReceiver import android.content.ContentResolver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.SharedPreferences import android.content.res.AssetFileDescriptor import android.database.Cursor @@ -21,6 +24,7 @@ import android.os.ProxyFileDescriptorCallback import android.os.storage.StorageManager import android.provider.DocumentsContract import android.provider.DocumentsProvider +import android.security.KeyChain import android.system.ErrnoException import android.system.Os import android.system.OsConstants @@ -310,6 +314,14 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference private val thumbnailTaskPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) + private val trustStoreListener = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == KeyChain.ACTION_TRUST_STORE_CHANGED) { + Rcbridge.rbReloadCerts() + } + } + } + private fun waitUntilUploadsDone(documentId: String) { val path = vfsPath(documentId) @@ -358,9 +370,15 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference Os.setenv("XDG_CACHE_HOME", context.cacheDir.path, true) Rcbridge.rbInit() + Rcbridge.rbReloadCerts() RcloneConfig.init(context) updateRcloneVerbosity() + context.registerReceiver( + trustStoreListener, + IntentFilter(KeyChain.ACTION_TRUST_STORE_CHANGED), + ) + return true } @@ -378,6 +396,8 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference } override fun shutdown() { + context!!.unregisterReceiver(trustStoreListener) + prefs.unregisterListener(this) thumbnailTaskPool.shutdown() diff --git a/rcbridge/rcbridge.go b/rcbridge/rcbridge.go index a163f4d..a98af6e 100644 --- a/rcbridge/rcbridge.go +++ b/rcbridge/rcbridge.go @@ -18,6 +18,10 @@ package rcbridge import ( // This package's init() MUST run first _ "rcbridge/envhack" + + "crypto/x509" + "encoding/pem" + "fmt" "strconv" "context" @@ -31,12 +35,14 @@ import ( "time" _ "golang.org/x/mobile/event/key" + "golang.org/x/sys/unix" _ "github.com/rclone/rclone/backend/all" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/cache" "github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config/obscure" + "github.com/rclone/rclone/fs/fshttp" "github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/operations" "github.com/rclone/rclone/fs/sync" @@ -82,8 +88,149 @@ var ( fs.ErrorFileNameTooLong: syscall.ENAMETOOLONG, config.ErrorConfigFileNotFound: syscall.ENOENT, } + caCertsLock goSync.Mutex + caCertsFile *os.File ) +// Load as many certificates from the PEM data as possible and return the last +// error, if any. +func parsePemCerts(data []byte) (certs []*x509.Certificate, err error) { + for len(data) > 0 { + var block *pem.Block + block, data = pem.Decode(data) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + certBytes := block.Bytes + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + continue + } + + certs = append(certs, cert) + } + + return certs, err +} + +// Generate a trust store in a single file that we can pass to rclone via its +// CaCert config option. This is necessary because golang currently does not +// support reading from the proper Android directories. We can't just set +// SSL_CERT_DIR either, even with envhack, because the user CA directory +// contains DER-encoded certificates and golang only supports loading PEM. +// +// Additionally, our implementation will not trust any system CA certificates +// that the user explicitly disabled from Android's settings. +// +// https://github.com/golang/go/issues/71258 +func generateTrustStoreTempFile(tempDir string) *os.File { + systemDir := os.Getenv("ANDROID_ROOT") + dataDir := os.Getenv("ANDROID_DATA") + + // This has never changed since 2011 when support for multi-user was added. + androidUid := os.Getuid() / 100_000 + + addDirs := []string{ + "/apex/com.android.conscrypt/cacerts", + systemDir + "/etc/security/cacerts", + fmt.Sprintf("%s/misc/user/%d/cacerts-added", dataDir, androidUid), + } + removeDirs := []string{ + fmt.Sprintf("%s/misc/user/%d/cacerts-removed", dataDir, androidUid), + } + + // Map from the certificate hash to the certificate path. + caFiles := make(map[string]string) + + // Add all available certificates. + for _, dir := range addDirs { + entries, err := os.ReadDir(dir) + if err != nil { + fs.Logf(nil, "Failed to read directory: %v: %v", dir, err) + continue + } + + for _, entry := range entries { + if !entry.Type().IsRegular() { + continue + } + + name := entry.Name() + + _, ok := caFiles[name] + if ok { + continue + } + + caFiles[name] = dir + "/" + name + } + } + + // And then remove all certificates disabled by the user. + for _, dir := range removeDirs { + entries, err := os.ReadDir(dir) + if err != nil { + fs.Logf(nil, "Failed to read directory: %v: %v", dir, err) + continue + } + + for _, entry := range entries { + if !entry.Type().IsRegular() { + continue + } + + delete(caFiles, entry.Name()) + } + } + + fd, err := unix.Open(tempDir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, 0600) + if err != nil { + fs.Errorf(nil, "Failed to create temp file in: %v: %v", tempDir, err) + return nil + } + + path := fmt.Sprintf("/proc/self/fd/%d", fd) + file := os.NewFile(uintptr(fd), path) + + for _, path := range caFiles { + data, err := os.ReadFile(path) + if err != nil { + fs.Logf(nil, "Failed to read file: %v: %v", path, err) + continue + } + + certs, err := x509.ParseCertificates(data) + if err != nil { + certs, err = parsePemCerts(data) + } + if err != nil { + fs.Logf(nil, "Failed to load certs: %v: %v", path, err) + continue + } + + for _, cert := range certs { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + + err = pem.Encode(file, block) + if err != nil { + fs.Logf(nil, "Failed to encode certificate: %v", cert) + continue + } + } + } + + fs.Logf(nil, "Loaded %d certificates", len(caFiles)) + + return file +} + // Initialize global aspects of the library. func RbInit() { librclone.Initialize() @@ -93,6 +240,34 @@ func RbInit() { ci.AskPassword = false } +// Reload certificates from the system and user trust stores. This is thread +// safe within RSAF, but may race with rclone's NewTransportCustom(). Note that +// reloading is not that useful because the changes do not affect fshttp clients +// that were previously created. +func RbReloadCerts() bool { + caCertsLock.Lock() + defer caCertsLock.Unlock() + + ci := fs.GetConfig(context.Background()) + ci.CaCert = nil + + if caCertsFile != nil { + caCertsFile.Close() + caCertsFile = nil + } + + caCertsFile = generateTrustStoreTempFile(config.GetCacheDir()) + if caCertsFile == nil { + return false + } + + ci.CaCert = append(ci.CaCert, caCertsFile.Name()) + + fshttp.ResetTransport() + + return true +} + // Clean up library resources. // // Note that this is a best-effort operation and it is impossible to completely