Skip to content
Open
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
52 changes: 48 additions & 4 deletions acquisition/acquisition.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,39 @@ func (a *Acquisition) PullToZipStaged(remotePath, zipPath string) error {
if err := a.validateStreamingMode(); err != nil {
return err
}
if a.StreamingPuller == nil {
return fmt.Errorf("streaming puller cannot be nil")
}
if remotePath == "" {
return fmt.Errorf("remote path cannot be empty")
}

return a.pullToZipStaged(zipPath, func(writer io.Writer) error {
return a.StreamingPuller.PullToWriter(remotePath, writer)
})
}

// SyncPullToZipStaged retrieves a device file through ADB's sync service and
// adds it to the acquisition archive only after the complete pull succeeds.
func (a *Acquisition) SyncPullToZipStaged(remotePath, zipPath string) error {
if err := a.validateStreamingMode(); err != nil {
return err
}
if adb.Client == nil {
return fmt.Errorf("ADB client cannot be nil")
}
if remotePath == "" {
return fmt.Errorf("remote path cannot be empty")
}

return a.pullToZipStaged(zipPath, func(writer io.Writer) error {
return adb.Client.SyncPullToWriter(remotePath, writer)
})
}

func (a *Acquisition) pullToZipStaged(zipPath string, pull func(io.Writer) error) error {
if a.ZipWriter.IsEncrypted() {
staged, err := a.StreamingPuller.PullToEncryptedTempFile(remotePath)
staged, err := createEncryptedTempFile(pull)
if err != nil {
return err
}
Expand All @@ -176,12 +206,26 @@ func (a *Acquisition) PullToZipStaged(remotePath, zipPath string) error {
return a.ZipWriter.CreateFileFromReader(zipPath, reader)
}

tempPath, err := a.StreamingPuller.PullToTempFile(remotePath)
tempFile, err := os.CreateTemp("", "androidqf-pull-*")
if err != nil {
return err
return fmt.Errorf("failed to create temporary file: %w", err)
}
tempPath := tempFile.Name()
defer os.Remove(tempPath)
return a.ZipWriter.CreateFileFromPath(zipPath, tempPath)

if err := pull(tempFile); err != nil {
_ = tempFile.Close()
return err
}
if err := tempFile.Close(); err != nil {
return fmt.Errorf("failed to close temporary file: %w", err)
}

err = a.ZipWriter.CreateFileFromPath(zipPath, tempPath)
if err != nil {
return err
}
return nil
}

func (a *Acquisition) GetSystemInformation() error {
Expand Down
78 changes: 78 additions & 0 deletions acquisition/acquisition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,84 @@ func TestCompleteReturnsArchiveFinalizationErrors(t *testing.T) {
}
}

func TestPullToZipStagedWithWriterSupportsEncryptedStaging(t *testing.T) {
var archive bytes.Buffer
writer := &StreamingZipWriter{
zipWriter: zip.NewWriter(&archive),
encrypted: true,
}
acq := &Acquisition{
ZipWriter: writer,
StreamingMode: true,
}
content := bytes.Repeat([]byte("sensitive policy data\n"), 4096)

err := acq.pullToZipStaged("selinux/sys/fs/selinux/policy", func(destination io.Writer) error {
_, err := destination.Write(content)
return err
})
if err != nil {
t.Fatalf("pullToZipStaged() error = %v", err)
}
if err := writer.zipWriter.Close(); err != nil {
t.Fatalf("zip Close() error = %v", err)
}

reader, err := zip.NewReader(bytes.NewReader(archive.Bytes()), int64(archive.Len()))
if err != nil {
t.Fatalf("zip.NewReader() error = %v", err)
}
if len(reader.File) != 1 || reader.File[0].Name != "selinux/sys/fs/selinux/policy" {
t.Fatalf("archive entries = %#v, want active SELinux policy", reader.File)
}
fileReader, err := reader.File[0].Open()
if err != nil {
t.Fatalf("Open(policy) error = %v", err)
}
got, err := io.ReadAll(fileReader)
_ = fileReader.Close()
if err != nil {
t.Fatalf("ReadAll(policy) error = %v", err)
}
if !bytes.Equal(got, content) {
t.Fatal("archived policy does not match pulled content")
}
}

func TestPullToZipStagedWithWriterDoesNotArchiveFailedEncryptedPull(t *testing.T) {
var archive bytes.Buffer
writer := &StreamingZipWriter{
zipWriter: zip.NewWriter(&archive),
encrypted: true,
}
acq := &Acquisition{
ZipWriter: writer,
StreamingMode: true,
}
pullErr := errors.New("partial sync failure")

err := acq.pullToZipStaged("selinux/sys/fs/selinux/policy", func(destination io.Writer) error {
if _, err := io.WriteString(destination, "partial policy"); err != nil {
return err
}
return pullErr
})
if !errors.Is(err, pullErr) {
t.Fatalf("pullToZipStaged() error = %v, want %v", err, pullErr)
}
if err := writer.zipWriter.Close(); err != nil {
t.Fatalf("zip Close() error = %v", err)
}

reader, err := zip.NewReader(bytes.NewReader(archive.Bytes()), int64(archive.Len()))
if err != nil {
t.Fatalf("zip.NewReader() error = %v", err)
}
if len(reader.File) != 0 {
t.Fatalf("archive contains entries after failed pull: %#v", reader.File)
}
}

func readZipFiles(t *testing.T, archivePath string) map[string]string {
t.Helper()

Expand Down
175 changes: 173 additions & 2 deletions adb/adb.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@
package adb

import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"os/exec"
"strconv"
"strings"
"time"

saveSlice "github.com/botherder/go-savetime/slice"
"github.com/mvt-project/androidqf/log"
)

type ADB struct {
ExePath string
Serial string
ExePath string
Serial string
serverAddress string
}

type DeviceInfo struct {
Expand All @@ -30,6 +36,12 @@ type DeviceInfo struct {

var Client *ADB

const (
defaultADBServerAddress = "127.0.0.1:5037"
maxSyncFrameSize = 1024 * 1024
adbDialTimeout = 10 * time.Second
)

// New returns a new ADB instance.
func New() (*ADB, error) {
adb := ADB{}
Expand Down Expand Up @@ -200,6 +212,165 @@ func (a *ADB) Pull(remotePath, localPath string) (string, error) {
return string(out), nil
}

// SyncPullToWriter retrieves a device file through ADB's sync service and
// writes it directly to writer. Unlike shell-based streaming, the sync service
// can retrieve files such as /sys/fs/selinux/policy that are exposed to
// `adb pull` but cannot be read from an ADB shell.
func (a *ADB) SyncPullToWriter(remotePath string, writer io.Writer) error {
if remotePath == "" {
return fmt.Errorf("remote path cannot be empty")
}
if writer == nil {
return fmt.Errorf("writer cannot be nil")
}

serverAddress := a.serverAddress
if serverAddress == "" {
serverAddress = defaultADBServerAddress
}

conn, err := net.DialTimeout("tcp", serverAddress, adbDialTimeout)
if err != nil {
return fmt.Errorf("failed to connect to ADB server: %w", err)
}
defer conn.Close()

transport := "host:transport-any"
if a.Serial != "" {
transport = "host:transport:" + a.Serial
}
if err := sendADBHostRequest(conn, transport); err != nil {
return fmt.Errorf("failed to select ADB transport: %w", err)
}
if err := sendADBHostRequest(conn, "sync:"); err != nil {
return fmt.Errorf("failed to start ADB sync service: %w", err)
}

if err := writeSyncRequest(conn, "RECV", remotePath); err != nil {
return fmt.Errorf("failed to request %q from ADB sync service: %w", remotePath, err)
}
if err := receiveSyncFile(conn, writer); err != nil {
return fmt.Errorf("failed to pull %q through ADB sync service: %w", remotePath, err)
}
return nil
}

func sendADBHostRequest(conn io.ReadWriter, request string) error {
if len(request) > 0xffff {
return fmt.Errorf("ADB request is too long")
}
header := fmt.Sprintf("%04x", len(request))
if err := writeString(conn, header+request); err != nil {
return err
}

status := make([]byte, 4)
if _, err := io.ReadFull(conn, status); err != nil {
return err
}
switch string(status) {
case "OKAY":
return nil
case "FAIL":
message, err := readADBHostFailure(conn)
if err != nil {
return err
}
return errors.New(message)
default:
return fmt.Errorf("unexpected ADB status %q", status)
}
}

func readADBHostFailure(reader io.Reader) (string, error) {
lengthBytes := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBytes); err != nil {
return "", err
}
length, err := strconv.ParseUint(string(lengthBytes), 16, 16)
if err != nil {
return "", fmt.Errorf("invalid ADB failure length %q", lengthBytes)
}
message := make([]byte, int(length))
if _, err := io.ReadFull(reader, message); err != nil {
return "", err
}
return string(message), nil
}

func writeSyncRequest(writer io.Writer, id, path string) error {
if len(id) != 4 {
return fmt.Errorf("sync request ID must be four bytes")
}
if len(path) > maxSyncFrameSize {
return fmt.Errorf("sync request path is too long")
}

header := make([]byte, 8)
copy(header[:4], id)
binary.LittleEndian.PutUint32(header[4:], uint32(len(path)))
if err := writeBytes(writer, header); err != nil {
return err
}
return writeString(writer, path)
}

func writeBytes(writer io.Writer, data []byte) error {
written, err := writer.Write(data)
if err != nil {
return err
}
if written != len(data) {
return io.ErrShortWrite
}
return nil
}

func writeString(writer io.Writer, data string) error {
written, err := io.WriteString(writer, data)
if err != nil {
return err
}
if written != len(data) {
return io.ErrShortWrite
}
return nil
}

func receiveSyncFile(reader io.Reader, writer io.Writer) error {
header := make([]byte, 8)
for {
if _, err := io.ReadFull(reader, header); err != nil {
return err
}

id := string(header[:4])
length := binary.LittleEndian.Uint32(header[4:])
switch id {
case "DATA":
if length > maxSyncFrameSize {
return fmt.Errorf("ADB sync DATA frame is too large: %d bytes", length)
}
if _, err := io.CopyN(writer, reader, int64(length)); err != nil {
return err
}
case "DONE":
return nil
case "FAIL":
if length > maxSyncFrameSize {
return fmt.Errorf("ADB sync FAIL frame is too large: %d bytes", length)
}
message := make([]byte, int(length))
if _, err := io.ReadFull(reader, message); err != nil {
return err
}
return errors.New(string(message))
default:
return fmt.Errorf("unexpected ADB sync response %q", id)
}
}
}

// Push a file on the phone
func (a *ADB) Push(localPath, remotePath string) (string, error) {
out, err := a.Exec("push", localPath, remotePath)
Expand Down
Loading
Loading