Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proper support for Android's CA trust stores #120

Merged
merged 1 commit into from
Jan 14, 2025
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
20 changes: 20 additions & 0 deletions app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
}

Expand All @@ -378,6 +396,8 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference
}

override fun shutdown() {
context!!.unregisterReceiver(trustStoreListener)

prefs.unregisterListener(this)

thumbnailTaskPool.shutdown()
Expand Down
175 changes: 175 additions & 0 deletions rcbridge/rcbridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ package rcbridge
import (
// This package's init() MUST run first
_ "rcbridge/envhack"

"crypto/x509"
"encoding/pem"
"fmt"
"strconv"

"context"
Expand All @@ -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"
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
Loading