implement IPC to privd

This commit is contained in:
Benjamin Wiegand
2026-02-12 20:58:19 -08:00
parent 749663db59
commit fbcdce51bb
22 changed files with 1259 additions and 14 deletions
+1
View File
@@ -34,6 +34,7 @@ android {
}
dependencies {
implementation(project(":libprivd"))
implementation(libs.appcompat)
implementation(libs.material)
+2
View File
@@ -10,6 +10,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- needed to launch arbitrary apps -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
@@ -26,11 +26,13 @@ import java.io.IOException;
import io.benwiegand.projection.geargrinder.logs.LogUiAdapter;
import io.benwiegand.projection.geargrinder.logs.LogcatReader;
import io.benwiegand.projection.geargrinder.privileged.RootPrivdLauncher;
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private LogcatReader logcatReader;
private RootPrivdLauncher privdLauncher;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -68,7 +70,6 @@ public class MainActivity extends AppCompatActivity {
.setAction(ConnectionRequestActivity.INTENT_ACTION_REQUEST_MEDIA_PROJECTION)));
findViewById(R.id.launch_privd_button).setOnClickListener(v -> {
RootPrivdLauncher privdLauncher = new RootPrivdLauncher(this);
try {
privdLauncher.launchRoot();
} catch (IOException e) {
@@ -115,6 +116,14 @@ public class MainActivity extends AppCompatActivity {
});
privdLauncher = new RootPrivdLauncher(this, connection -> {
Log.i(TAG, "IPC connected");
connection.send(IPCConstants.COMMAND_PING)
.doOnResult(r -> Log.i(TAG, "pong from daemon"))
.doOnError(t -> Log.e(TAG, "failed to ping daemon", t))
.callMeWhenDone();
});
RecyclerView logRecyclerView = findViewById(R.id.log_recycler);
LogUiAdapter logUiAdapter = new LogUiAdapter();
@@ -0,0 +1,7 @@
package io.benwiegand.projection.geargrinder.callback;
import io.benwiegand.projection.geargrinder.privileged.PrivdIPCConnection;
public interface IPCConnectionListener {
void onPrivdConnected(PrivdIPCConnection connection);
}
@@ -0,0 +1,161 @@
package io.benwiegand.projection.geargrinder.privileged;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.SecureRandom;
import io.benwiegand.projection.geargrinder.callback.IPCConnectionListener;
public class IPCServer {
private static final String TAG = IPCServer.class.getSimpleName();
private static final InetAddress BIND_ADDRESS = InetAddress.getLoopbackAddress();
private static final int BIND_PORT = 0; // auto
private static final int MAX_CONNECTIONS = 5; // for resiliency if something else connects first
private static final int AUTHENTICATION_SECRET_LENGTH = 1024;
private final Thread serverThread;
private final ServerSocket server;
private int port = -1;
private boolean dead = false;
private final SecureRandom random;
private final byte[] tokenA = new byte[AUTHENTICATION_SECRET_LENGTH];
private final byte[] tokenB = new byte[AUTHENTICATION_SECRET_LENGTH];
private final PrivdIPCConnection[] connections = new PrivdIPCConnection[MAX_CONNECTIONS];
private int activeConnection = 0;
private final IPCConnectionListener connectionListener;
public IPCServer(IPCConnectionListener connectionListener) {
this.connectionListener = connectionListener;
try {
serverThread = new Thread(this::serverLoop, "geargrinder-ipc-server");
// server = SSLServerSocketFactory.getDefault().createServerSocket();
server = new ServerSocket();
random = SecureRandom.getInstanceStrong();
rotate();
} catch (Throwable t) {
Log.e(TAG, "failed to init IPC server", t);
throw new RuntimeException(t);
}
}
public interface ConnectionInitCallback {
void onInitComplete(boolean success);
}
public void close() {
if (dead) return;
dead = true;
serverThread.interrupt();
try {
if (!server.isClosed())
server.close();
} catch (IOException e) {
Log.e(TAG, "IOException while closing socket", e);
}
synchronized (connections) {
for (int i = 0; i < connections.length; i++) {
if (connections[i] == null) continue;
connections[i].close();
connections[i] = null;
}
}
}
public void rotate() {
// keeps the same port number but with different tokens.
// this is preferred over spinning up a new server for security.
Log.i(TAG, "generating new tokens");
random.nextBytes(tokenA);
random.nextBytes(tokenB);
}
public void start() throws IOException {
server.bind(new InetSocketAddress(BIND_ADDRESS, BIND_PORT));
Log.i(TAG, "IPC server started on " + server.getLocalSocketAddress());
port = server.getLocalPort();
serverThread.start();
}
public byte[] getTokenA() {
return tokenA;
}
public byte[] getTokenB() {
return tokenB;
}
public int getPort() {
return port;
}
public PrivdIPCConnection getActiveConnection() {
synchronized (connections) {
if (connections[activeConnection] == null) return null;
if (!connections[activeConnection].isAlive()) return null;
return connections[activeConnection];
}
}
private int findFreeConnectionIdLocked() {
int id = -1;
for (int i = 0; i < connections.length; i++) {
if (connections[i] != null) continue;
id = i;
break;
}
return id;
}
private void serverLoop() {
try {
Log.d(TAG, "server loop start");
while (!dead) {
Socket socket = server.accept();
if (getActiveConnection() != null) {
Log.e(TAG, "already connected! refusing connection from " + socket.getRemoteSocketAddress());
try {
socket.close();
} catch (Throwable ignored) {}
continue;
}
synchronized (connections) {
Log.d(TAG, "connection from " + socket.getRemoteSocketAddress());
int connectionId = findFreeConnectionIdLocked();
if (connectionId == -1) {
Log.e(TAG, "too many open connections, dropping");
continue;
}
connections[connectionId] = new PrivdIPCConnection(socket, tokenA, tokenB, success -> {
synchronized (connections) {
if (!success) {
connections[connectionId] = null;
return;
}
activeConnection = connectionId;
connectionListener.onPrivdConnected(connections[connectionId]);
}
});
}
}
} catch (IOException e) {
Log.e(TAG, "IOException during IPC server loop", e);
} finally {
close();
}
}
}
@@ -0,0 +1,50 @@
package io.benwiegand.projection.geargrinder.privileged;
import android.util.Log;
import java.io.IOException;
import java.net.Socket;
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
public class PrivdIPCConnection extends IPCConnection {
private static final String TAG = PrivdIPCConnection.class.getSimpleName();
private final IPCServer.ConnectionInitCallback initCallback;
public PrivdIPCConnection(Socket socket, byte[] tokenA, byte[] tokenB, IPCServer.ConnectionInitCallback initCallback) throws IOException {
super(socket, tokenA, tokenB);
this.initCallback = initCallback;
}
@Override
protected String getTag() {
return TAG;
}
@Override
protected void onInitComplete(boolean success) {
initCallback.onInitComplete(success);
}
@Override
protected Reply onCommand(int command, byte[] data, int offset, int length) {
return switch (command) {
case IPCConstants.COMMAND_PING -> {
Log.d(TAG, "ping");
yield new Reply(IPCConstants.REPLY_SUCCESS);
}
default -> {
Log.wtf(TAG, "unhandled command: " + command);
yield new Reply(IPCConstants.REPLY_FAILURE);
}
};
}
@Override
protected void onClose() {
// TODO
Log.e(TAG, "connection closed");
}
}
@@ -1,6 +1,11 @@
package io.benwiegand.projection.geargrinder.privileged;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_PORT;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_A;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_B;
import android.content.Context;
import android.util.Base64;
import android.util.Log;
import java.io.File;
@@ -8,41 +13,75 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import io.benwiegand.projection.geargrinder.callback.IPCConnectionListener;
public class RootPrivdLauncher {
private static final String TAG = RootPrivdLauncher.class.getSimpleName();
private static final String PRIVD_FILE_NAME = "privd.jar";
private static final String ENV_CLASSPATH = "CLASSPATH";
private final Context context;
private final IPCConnectionListener connectionListener;
private final File daemonFile;
private IPCServer server = null;
private Process rootProcess = null;
public RootPrivdLauncher(Context context) {
public RootPrivdLauncher(Context context, IPCConnectionListener connectionListener) {
this.context = context;
this.connectionListener = connectionListener;
daemonFile = context.getFilesDir().toPath().resolve(PRIVD_FILE_NAME).toFile();
}
public void destroy() {
killRootProcess();
killServer();
}
public void launchRoot() throws IOException {
killRootProcess();
copyDaemon();
startServer();
executeDaemon();
}
Log.i(TAG, "launching privd as root");
ProcessBuilder procBuilder = new ProcessBuilder("su", "-c", "app_process /system/bin --nice-name=Geargrinder-privd io.benwiegand.projection.geargrinder.privd.Main");
procBuilder.environment().put("CLASSPATH", daemonFile.getAbsolutePath());
rootProcess = procBuilder.start();
private void killServer() {
if (server == null) return;
Log.i(TAG, "closing IPC server");
server.close();
server = null;
}
private void killRootProcess() {
if (rootProcess != null) {
Log.i(TAG, "killing privd root process");
rootProcess.destroyForcibly();
rootProcess = null;
if (rootProcess == null) return;
Log.i(TAG, "killing privd root process");
rootProcess.destroyForcibly();
rootProcess = null;
}
private void startServer() throws IOException {
if (server != null) {
server.rotate();
return;
}
Log.i(TAG, "starting IPC server");
server = new IPCServer(connectionListener);
server.start();
}
private void executeDaemon() throws IOException {
assert rootProcess == null;
assert server != null;
Log.i(TAG, "launching privd as root");
ProcessBuilder procBuilder = new ProcessBuilder("su", "-c", "app_process /system/bin --nice-name=Geargrinder-privd io.benwiegand.projection.geargrinder.privd.Main");
procBuilder.environment().put(ENV_CLASSPATH, daemonFile.getAbsolutePath());
procBuilder.environment().put(ENV_PORT, String.valueOf(server.getPort()));
// tokens are swapped for app/daemon
procBuilder.environment().put(ENV_TOKEN_A, Base64.encodeToString(server.getTokenB(), Base64.NO_WRAP));
procBuilder.environment().put(ENV_TOKEN_B, Base64.encodeToString(server.getTokenA(), Base64.NO_WRAP));
rootProcess = procBuilder.start();
}
private void copyDaemon() throws IOException {
+1
View File
@@ -19,4 +19,5 @@ localbroadcastmanager = { group = "androidx.localbroadcastmanager", name = "loca
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library" }
+3
View File
@@ -0,0 +1,3 @@
/build
/release
/debug
+17
View File
@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "io.benwiegand.projection.libprivd"
compileSdk = 36
defaultConfig {
minSdk = 26
targetSdk = 36
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
@@ -0,0 +1,287 @@
package io.benwiegand.projection.libprivd.ipc;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.benwiegand.projection.libprivd.sec.Sec;
import io.benwiegand.projection.libprivd.sec.SecAdapter;
public abstract class IPCConnection {
private final String TAG = getTag();
private static final int AUTH_TIMEOUT = 1000;
private final Map<Integer, SecAdapter<Reply>> pendingReplyMap = new ConcurrentHashMap<>();
private final Object writeLock = new Object();
private final Thread connectionThread;
private final Socket socket;
private final InputStream is;
private final OutputStream os;
private final CountDownLatch initLatch = new CountDownLatch(1);
private final byte[] readBuffer = new byte[IPCMessage.HEADER_LENGTH + 65535];
private final byte[] writeBuffer = new byte[IPCMessage.HEADER_LENGTH + 65535];
private final byte[] tokenA;
private final byte[] tokenB;
private int msgIdCounter = 0;
private boolean dead = false;
public IPCConnection(Socket socket, byte[] tokenA, byte[] tokenB) throws IOException {
this.connectionThread = new Thread(this::connectionLoop, "geargrinder-ipc");
this.socket = socket;
is = socket.getInputStream();
os = socket.getOutputStream();
this.tokenA = tokenA;
this.tokenB = tokenB;
connectionThread.start();
}
public void close() {
if (dead) return;
try {
if (!socket.isClosed())
socket.close();
} catch (IOException e) {
Log.e(TAG, "IOException while closing socket", e);
}
dead = true;
connectionThread.interrupt();
while (!pendingReplyMap.isEmpty()) {
Set<Integer> messageIds = new HashSet<>(pendingReplyMap.keySet());
for (int msgId : messageIds) {
SecAdapter<Reply> adapter = pendingReplyMap.remove(msgId);
assert adapter != null;
adapter.throwError(new IOException("connection closed"));
}
}
onClose();
}
public static final class Reply {
public final int status;
public final byte[] data;
public final int offset;
public final int length;
public Reply(int status, byte[] data, int offset, int length) {
this.status = status;
this.data = data;
this.offset = offset;
this.length = length;
}
public Reply(int status, byte[] data) {
this(status, data, 0, data.length);
}
public Reply(int status) {
this(status, new byte[0]);
}
public static Reply copyFromMessage(IPCMessage msg) {
assert msg.isReply();
byte[] data = new byte[msg.getDataLength()];
System.arraycopy(msg.getBuffer(), IPCMessage.HEADER_LENGTH, data, 0, data.length);
return new Reply(msg.getCommand(), data);
}
}
protected abstract String getTag();
protected void onInitComplete(boolean success) {
}
protected abstract Reply onCommand(int command, byte[] data, int offset, int length);
protected abstract void onClose();
public void waitForInit(long timeout) throws InterruptedException, TimeoutException {
if (!initLatch.await(timeout, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("timed out waiting for IPC init");
}
}
public boolean isAlive() {
return !dead && initLatch.getCount() != 1 && socket.isConnected();
}
public Sec<Reply> send(int command, byte[] data, int offset, int length) {
if (dead) return Sec.premeditatedError(new IOException("connection closed"));
SecAdapter.SecWithAdapter<Reply> secWithAdapter = SecAdapter.createThreadless();
synchronized (writeLock) {
int msgId = nextMsgId();
pendingReplyMap.put(msgId, secWithAdapter.secAdapter());
try {
writeMsg(new IPCMessage(writeBuffer)
.clear()
.setMessageId(msgId)
.setCommand(command)
.copyData(data, offset, length));
} catch (Throwable t) {
pendingReplyMap.remove(msgId);
return Sec.premeditatedError(t);
}
}
return secWithAdapter.sec();
}
public Sec<Reply> send(int command, byte[] data) {
return send(command, data, 0, data.length);
}
public Sec<Reply> send(int command, Parcelable parcelable) {
Parcel parcel = Parcel.obtain();
parcelable.writeToParcel(parcel, 0);
Sec<Reply> sec = send(command, parcel.marshall());
parcel.recycle();
return sec;
}
public Sec<Reply> send(int command) {
return send(command, new byte[0]);
}
private int nextMsgId() {
if (msgIdCounter++ >= 0xffff) msgIdCounter = 0;
return msgIdCounter;
}
private void readAll(InputStream is, byte[] buffer, int offset, int length) throws IOException {
int len, i = 0;
while (i < length) {
len = is.read(buffer, i + offset, length - i);
if (len < 0) throw new IOException("EOS (" + len + ")");
i += len;
}
}
private void readAllTimeout(InputStream is, byte[] buffer, int offset, int length, int timeout) throws IOException {
int initialSocketTimeout = socket.getSoTimeout();
long deadline = SystemClock.elapsedRealtime() + timeout;
int len, i = 0;
try {
while (i < length && SystemClock.elapsedRealtime() < deadline) {
int remaining = (int) (deadline - SystemClock.elapsedRealtime());
if (remaining > 0) socket.setSoTimeout(remaining);
len = is.read(buffer, i + offset, length - i);
if (len < 0) throw new IOException("EOS (" + len + ")");
i += len;
}
} catch (SocketTimeoutException e) {
throw new IOException("read timed out", e);
}
if (i < length) throw new IOException("read timed out");
socket.setSoTimeout(initialSocketTimeout);
}
private void writeMsg(IPCMessage msg) throws IOException {
os.write(msg.getBuffer(), 0, IPCMessage.HEADER_LENGTH + msg.getDataLength());
}
private void readMsg(IPCMessage msg) throws IOException {
readAll(is, msg.getBuffer(), 0, IPCMessage.HEADER_LENGTH);
if (msg.getDataLength() > 0)
readAll(is, msg.getBuffer(), IPCMessage.HEADER_LENGTH, msg.getDataLength());
}
private void sendReply(Reply reply, int messageId) throws IOException {
synchronized (writeLock) {
writeMsg(new IPCMessage(writeBuffer)
.clear()
.setMessageId(messageId)
.setFlags(IPCConstants.FLAG_REPLY)
.setCommand(reply.status)
.copyData(reply.data, reply.offset, reply.length));
}
}
private void connectionLoop() {
// read loop, write is handled by calling thread
Log.d(TAG, "start connection loop");
try {
// auth
socket.setSoTimeout(AUTH_TIMEOUT);
os.write(tokenA, 0, tokenA.length);
readAllTimeout(is, readBuffer, 0, tokenB.length, AUTH_TIMEOUT);
boolean valid = true;
for (int i = 0; i < tokenB.length; i++) {
if (tokenB[i] == readBuffer[i]) continue;
valid = false;
// don't break: timing could give away token
}
if (!valid) {
Log.e(TAG, "auth failed for " + socket.getRemoteSocketAddress());
return;
}
Log.i(TAG, "auth succeeded for " + socket.getRemoteSocketAddress());
socket.setSoTimeout(0);
initLatch.countDown();
onInitComplete(true);
IPCMessage msg = new IPCMessage(readBuffer);
while (socket.isConnected()) {
readMsg(msg);
if (msg.isReply()) {
SecAdapter<Reply> adapter = pendingReplyMap.remove(msg.getMessageId());
if (adapter == null) {
Log.wtf(TAG, "couldn't find pending reply entry for reply message: " + msg);
continue;
}
adapter.provideResult(Reply.copyFromMessage(msg));
continue;
}
Reply reply = onCommand(msg.getCommand(), msg.getBuffer(), IPCMessage.HEADER_LENGTH, msg.getDataLength());
sendReply(reply, msg.getMessageId());
}
} catch (IOException e) {
Log.e(TAG, "IOException in connection loop", e);
} finally {
if (initLatch.getCount() > 0) {
initLatch.countDown();
onInitComplete(false);
}
close();
}
}
}
@@ -0,0 +1,17 @@
package io.benwiegand.projection.libprivd.ipc;
public class IPCConstants {
public static final long PING_INTERVAL = 1000;
public static final String ENV_PORT = "PORT";
public static final String ENV_TOKEN_A = "TOKEN_A";
public static final String ENV_TOKEN_B = "TOKEN_B";
public static final int FLAG_REPLY = 1;
public static final int COMMAND_PING = 0;
public static final int REPLY_SUCCESS = 0;
public static final int REPLY_FAILURE = 1;
}
@@ -0,0 +1,92 @@
package io.benwiegand.projection.libprivd.ipc;
import static io.benwiegand.projection.libprivd.util.ByteUtil.readUInt16;
import static io.benwiegand.projection.libprivd.util.ByteUtil.writeUInt16;
import java.util.Arrays;
public final class IPCMessage {
private static final int MESSAGE_ID_OFFSET = 0;
private static final int FLAGS_OFFSET = 2;
private static final int CMD_OFFSET = 3;
private static final int DATA_LEN_OFFSET = 4;
private static final int DATA_OFFSET = 6;
public static final int HEADER_LENGTH = 6;
private final byte[] buffer;
public IPCMessage(byte[] buffer) {
this.buffer = buffer;
}
public byte[] getBuffer() {
return buffer;
}
public int getMessageId() {
return readUInt16(buffer, MESSAGE_ID_OFFSET);
}
public IPCMessage setMessageId(int msgId) {
assert msgId >= 0 && msgId <= 0xffff;
writeUInt16(msgId, buffer, MESSAGE_ID_OFFSET);
return this;
}
public int getFlags() {
return buffer[FLAGS_OFFSET];
}
public IPCMessage setFlags(int flags) {
assert flags >= 0 && flags <= 0xff;
buffer[FLAGS_OFFSET] = (byte) flags;
return this;
}
public int getCommand() {
return buffer[CMD_OFFSET];
}
public IPCMessage setCommand(int cmd) {
assert cmd >= 0 && cmd <= 0xff;
buffer[CMD_OFFSET] = (byte) cmd;
return this;
}
public int getDataLength() {
return readUInt16(buffer, DATA_LEN_OFFSET);
}
public IPCMessage setDataLength(int dataLength) {
assert dataLength >= 0 && dataLength <= 0xffff;
writeUInt16(dataLength, buffer, DATA_LEN_OFFSET);
return this;
}
public boolean isReply() {
return (getFlags() & IPCConstants.FLAG_REPLY) != 0;
}
public IPCMessage copyData(byte[] src, int offset, int length) {
System.arraycopy(src, offset, buffer, DATA_OFFSET, length);
setDataLength(length);
return this;
}
public IPCMessage clear() {
Arrays.fill(buffer, 0, HEADER_LENGTH, (byte) 0);
return this;
}
@Override
public String toString() {
return "IPCMessage{" +
"buffer=(" + buffer.length + " bytes)" +
", messageId=" + getMessageId() +
", flags=" + getFlags() +
", command=" + getCommand() +
", dataLength=" + getDataLength() +
'}';
}
}
@@ -0,0 +1,61 @@
package io.benwiegand.projection.libprivd.sec;
import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Function;
import java.util.function.Supplier;
public class PendingSec<T> {
private final Object lock = new Object();
private boolean started = false;
private boolean wrapped = false;
private final Supplier<Sec<T>> secStarter;
PendingSec(Supplier<Sec<T>> secStarter) {
this.secStarter = secStarter;
}
public Sec<T> start() {
synchronized (lock) {
if (started) throw new IllegalStateException("already started");
started = true;
}
return secStarter.get();
}
private <U> PendingSec<U> wrapSec(Function<Sec<T>, Sec<U>> mapping) {
synchronized (lock) {
if (started) throw new IllegalStateException("can't wrap after call to start()");
if (wrapped) throw new IllegalStateException("already wrapped!");
wrapped = true;
return new PendingSec<>(() -> mapping.apply(this.start()));
}
}
public <U> PendingSec<U> flatMapSec(Function<T, Sec<U>> mapping) {
return wrapSec(sec -> sec.flatMap(mapping));
}
public <U> PendingSec<U> mapSec(Function<T, U> mapping) {
return wrapSec(sec -> sec.map(mapping));
}
public <U> PendingSec<U> flatMap(Function<T, PendingSec<U>> mapping) {
return flatMapSec(r -> mapping.apply(r).start());
}
public static PendingSec<Void> createDelay(long delay) {
return new PendingSec<>(() -> {
SecAdapter.SecWithAdapter<Void> secWithAdapter = SecAdapter.createThreadless();
boolean started = new Handler(Looper.getMainLooper())
.postDelayed(() -> secWithAdapter.secAdapter().provideResult(null), delay);
if (!started) return Sec.premeditatedError(new RejectedExecutionException("main looper is dead"));
return secWithAdapter.sec();
});
}
}
@@ -0,0 +1,233 @@
package io.benwiegand.projection.libprivd.sec;
import static io.benwiegand.projection.libprivd.sec.SecAdapter.createThreadless;
import android.util.Log;
import java.util.function.Consumer;
import java.util.function.Function;
// as in "just a sec"
public class Sec<T> {
private static final String TAG = Sec.class.getSimpleName();
private final Object lock = new Object();
private boolean finished = false;
private T result = null;
private Throwable error = null;
private boolean callbacksSet = false;
private boolean callbacksCalled = false;
private Consumer<T> onResult = null;
private Consumer<Throwable> onError = null;
Sec() {}
Adapter createAdapter() {
return new Adapter();
}
public boolean isFinished() {
synchronized (lock) {
return finished;
}
}
public boolean isSuccessful() {
synchronized (lock) {
if (!finished) throw new IllegalStateException("not finished, success is not yet known");
return error == null;
}
}
public T getResult() {
synchronized (lock) {
if (!finished) throw new IllegalStateException("not finished, result doesn't exist yet");
return result;
}
}
public Throwable getError() {
synchronized (lock) {
if (!finished) throw new IllegalStateException("not finished, error doesn't exist yet");
return error;
}
}
public T getResultOrThrow() throws Throwable {
synchronized (lock) {
if (!finished) throw new IllegalStateException("not finished, result/error doesn't exist yet");
if (error != null) throw error;
return result;
}
}
public Sec<T> doOnResult(Consumer<T> onResult) {
synchronized (lock) {
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
this.onResult = onResult;
return this;
}
}
public Sec<T> doOnError(Consumer<Throwable> onError) {
synchronized (lock) {
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
this.onError = onError;
return this;
}
}
private static <T, U> Consumer<T> applyMap(Function<T, U> mapper, Consumer<U> downstreamResult, Consumer<Throwable> downstreamError) {
return r -> {
U mapped;
try {
mapped = mapper.apply(r);
} catch (Throwable t) {
downstreamError.accept(t);
return;
}
downstreamResult.accept(mapped);
};
}
public <U> Sec<U> map(Function<T, U> map) {
// for now just use the callbacks. this may change in the future
SecAdapter.SecWithAdapter<U> secWithAdapter;
synchronized (lock) {
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
callbacksSet = true;
secWithAdapter = createThreadless();
SecAdapter<U> adapter = secWithAdapter.secAdapter();
this.onResult = applyMap(map, adapter::provideResult, adapter::throwError);
this.onError = adapter::throwError;
}
if (isFinished()) callCallbacks();
return secWithAdapter.sec();
}
public Sec<T> mapError(Function<Throwable, Throwable> map) {
// for now just use the callbacks. this may change in the future
SecAdapter.SecWithAdapter<T> secWithAdapter;
synchronized (lock) {
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
callbacksSet = true;
secWithAdapter = createThreadless();
SecAdapter<T> adapter = secWithAdapter.secAdapter();
this.onResult = adapter::provideResult;
this.onError = applyMap(map, adapter::throwError, adapter::throwError);
}
if (isFinished()) callCallbacks();
return secWithAdapter.sec();
}
public <U> Sec<U> flatMap(Function<T, Sec<U>> map) {
// for now just use the callbacks. this may change in the future
SecAdapter.SecWithAdapter<U> secWithAdapter;
synchronized (lock) {
if (callbacksSet) throw new IllegalStateException("callbacks already set up");
callbacksSet = true;
secWithAdapter = createThreadless();
SecAdapter<U> adapter = secWithAdapter.secAdapter();
this.onResult = applyMap(
map,
nextSec -> nextSec
.doOnResult(adapter::provideResult)
.doOnError(adapter::throwError)
.callMeWhenDone(),
adapter::throwError);
this.onError = adapter::throwError;
}
if (isFinished()) callCallbacks();
return secWithAdapter.sec();
}
public void callMeWhenDone() {
synchronized (lock) {
if (callbacksSet) throw new IllegalStateException("callMeWhenDone() cannot be called twice");
callbacksSet = true;
}
if (isFinished()) callCallbacks();
}
private void callCallbacks() {
synchronized (lock) {
assert finished;
if (!callbacksSet) return; // callbacks aren't ready
if (callbacksCalled) return; // don't call back twice
callbacksCalled = true;
}
try {
if (error == null && onResult != null)
onResult.accept(result);
} catch (Throwable t) {
Log.e(TAG, "error during onResult callback", t);
}
try {
if (error != null && onError != null)
onError.accept(error);
} catch (Throwable t) {
Log.e(TAG, "error during onError callback", t);
}
}
private class Adapter implements SecAdapter<T> {
@Override
public void provideResult(T r) {
synchronized (lock) {
if (finished) throw new IllegalStateException("a result or error has already been provided");
finished = true;
result = r;
}
callCallbacks();
}
@Override
public void throwError(Throwable t) {
if (t == null) throw new IllegalArgumentException("throwable cannot be null");
synchronized (lock) {
if (finished) throw new IllegalStateException("a result or error has already been provided");
finished = true;
error = t;
}
callCallbacks();
}
}
public static <T> Sec<T> premeditatedError(Throwable t) {
SecAdapter.SecWithAdapter<T> secWithAdapter = createThreadless();
secWithAdapter.secAdapter().throwError(t);
return secWithAdapter.sec();
}
public static <T> Sec<T> premeditated(T r) {
SecAdapter.SecWithAdapter<T> secWithAdapter = createThreadless();
secWithAdapter.secAdapter().provideResult(r);
return secWithAdapter.sec();
}
}
@@ -0,0 +1,84 @@
package io.benwiegand.projection.libprivd.sec;
import android.os.Handler;
import android.util.Log;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Consumer;
public interface SecAdapter<T> {
String TAG = SecAdapter.class.getSimpleName();
void provideResult(T result);
void throwError(Throwable t);
final class SecWithAdapter<T> {
private final Sec<T> sec;
private final SecAdapter<T> secAdapter;
public SecWithAdapter(Sec<T> sec, SecAdapter<T> secAdapter) {
this.sec = sec;
this.secAdapter = secAdapter;
}
public Sec<T> sec() {
return sec;
}
public SecAdapter<T> secAdapter() {
return secAdapter;
}
}
static <T> SecWithAdapter<T> createThreadless() {
Sec<T> sec = new Sec<>();
SecAdapter<T> adapter = sec.createAdapter();
return new SecWithAdapter<>(sec, adapter);
}
private static Executor handlerAsExecutor(Handler handler) {
return runnable -> {
if (!handler.post(runnable))
throw new RejectedExecutionException("handler is dead");
};
}
static <T> PendingSec<T> create(Executor executor, Consumer<SecAdapter<T>> deferredResult) {
return new PendingSec<>(() -> {
Sec<T> sec = new Sec<>();
try {
executor.execute(() -> {
SecAdapter<T> adapter = sec.createAdapter();
try {
deferredResult.accept(adapter);
} catch (Throwable t) {
// this situation should generally be avoided
Log.wtf("SecAdapter", "deferred sec handler threw!", t);
try {
// ensure sec at least gets finished
if (!sec.isFinished()) adapter.throwError(t);
} catch (Throwable ignored) {}
// this will crash the app anyway
throw t;
}
});
} catch (Throwable t) {
return Sec.premeditatedError(t);
}
return sec;
});
}
static <T> PendingSec<T> create(Handler handler, Consumer<SecAdapter<T>> deferredResult) {
return create(handlerAsExecutor(handler), deferredResult);
}
}
@@ -0,0 +1,20 @@
package io.benwiegand.projection.libprivd.util;
public class ByteUtil {
public static int unsignByte(byte b) {
return Byte.toUnsignedInt(b);
}
public static int writeUInt16(int value, byte[] buffer, int offset) {
assert value >= 0;
assert value <= 0xFFFF;
buffer[offset] = (byte) (value >> 8);
buffer[offset+1] = (byte) (value & 0xff);
return 2;
}
public static int readUInt16(byte[] buffer, int offset) {
return (unsignByte(buffer[offset]) << 8) + unsignByte(buffer[offset + 1]);
}
}
+10 -1
View File
@@ -26,21 +26,30 @@ androidComponents {
group = "build"
description = "privileged daemon jar asset"
dependsOn(tasks.build)
dependsOn(":libprivd:build")
val outputJar = rootProject.projectDir.resolve("app/src/main/assets/privd.jar")
val androidJar = "${android.sdkDirectory.path}/platforms/android-${android.defaultConfig.targetSdk}/android.jar"
val classesDir = layout.buildDirectory.dir("intermediates/javac/${buildType}/compile${buildTypeUpper}JavaWithJavac/classes")
val libClassesDir = rootProject.projectDir.resolve("libprivd/build/intermediates/javac/${buildType}/compile${buildTypeUpper}JavaWithJavac/classes")
val classFiles = Files.walk(file(classesDir).toPath())
.filter { it.toFile().isFile() }
.toArray()
val libClassFiles = Files.walk(file(libClassesDir).toPath())
.filter { it.toFile().isFile() }
.toArray()
commandLine(
"${android.sdkDirectory}/build-tools/${android.buildToolsVersion}/d8",
"--release",
"--output", outputJar,
"--classpath", androidJar,
*classFiles
*classFiles, *libClassFiles
)
}
}
}
dependencies {
implementation(project(":libprivd"))
}
@@ -1,28 +1,98 @@
package io.benwiegand.projection.geargrinder.privd;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.COMMAND_PING;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_PORT;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_A;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.ENV_TOKEN_B;
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.PING_INTERVAL;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
import io.benwiegand.projection.geargrinder.privd.reflection.ReflectionException;
import io.benwiegand.projection.geargrinder.privd.ipc.IPCClient;
import io.benwiegand.projection.geargrinder.privd.reflection.reflected.ReflectedActivityThread;
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
public class Main {
private static final String TAG = "privd-" + Main.class.getSimpleName();
private static final long IPC_INIT_TIMEOUT = 5000;
private static void pingTask(Handler handler, IPCConnection connection) {
connection.send(COMMAND_PING)
.doOnResult(reply -> Log.i(TAG, "pong: " + reply.status))
.doOnError(t -> {
Log.e(TAG, "failed to ping", t);
System.exit(1);
})
.callMeWhenDone();
if (!handler.postDelayed(() -> pingTask(handler, connection), PING_INTERVAL)) {
Log.w(TAG, "failed to post next ping task, bailing");
System.exit(0);
}
}
public static void main(String[] args) {
Log.wtf("Testing123", "hello from privd");
Log.i(TAG, "Geargrinder privd");
int port;
byte[] tokenA;
byte[] tokenB;
try {
String portString = System.getenv(ENV_PORT);
String tokenAString = System.getenv(ENV_TOKEN_A);
String tokenBString = System.getenv(ENV_TOKEN_B);
if (portString == null || tokenAString == null || tokenBString == null)
throw new AssertionError("missing required environment variable(s)");
port = Integer.parseInt(portString);
tokenA = Base64.decode(tokenAString, 0);
tokenB = Base64.decode(tokenBString, 0);
} catch (Throwable t) {
Log.e(TAG, "failed to parse environment", t);
System.exit(1);
return;
}
IPCClient ipcClient;
IPCConnection connection;
try {
ipcClient = new IPCClient(port, tokenA, tokenB);
connection = ipcClient.connect();
connection.waitForInit(IPC_INIT_TIMEOUT);
} catch (Throwable t) {
Log.e(TAG, "failed to open IPC connection", t);
System.exit(1);
return;
}
Looper.prepareMainLooper();
Handler handler = new Handler(Looper.getMainLooper());
if (!handler.post(() -> pingTask(handler, connection))) {
Log.e(TAG, "failed to post ping task");
System.exit(1);
}
try {
ReflectedActivityThread activityThread = new ReflectedActivityThread();
Context context = activityThread.getSystemContext();
System.out.println("got a system context: " + context);
Log.i(TAG, "got a system context: " + context);
} catch (ReflectionException e) {
throw new RuntimeException(e);
Log.e(TAG, "failed to get system context", e);
System.exit(1);
}
Looper.loop();
}
}
@@ -0,0 +1,43 @@
package io.benwiegand.projection.geargrinder.privd.ipc;
import android.util.Log;
import java.io.IOException;
import java.net.Socket;
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
public class AppIPCConnection extends IPCConnection {
private static final String TAG = AppIPCConnection.class.getSimpleName();
public AppIPCConnection(Socket socket, byte[] tokenA, byte[] tokenB) throws IOException {
super(socket, tokenA, tokenB);
}
@Override
protected String getTag() {
return TAG;
}
@Override
protected Reply onCommand(int command, byte[] data, int offset, int length) {
return switch (command) {
case IPCConstants.COMMAND_PING -> {
Log.d(TAG, "ping");
yield new Reply(IPCConstants.REPLY_SUCCESS);
}
default -> {
Log.wtf(TAG, "unhandled command: " + command);
yield new Reply(IPCConstants.REPLY_FAILURE);
}
};
}
@Override
protected void onClose() {
// daemon has no reason to run without an app
Log.e(TAG, "connection closed");
System.exit(0);
}
}
@@ -0,0 +1,38 @@
package io.benwiegand.projection.geargrinder.privd.ipc;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import io.benwiegand.projection.libprivd.ipc.IPCConnection;
public class IPCClient {
private static final String TAG = IPCClient.class.getSimpleName();
private static final InetAddress SERVER_ADDRESS = InetAddress.getLoopbackAddress();
private final Socket socket;
private final int port;
private final byte[] tokenA;
private final byte[] tokenB;
public IPCClient(int port, byte[] tokenA, byte[] tokenB) throws IOException {
// TODO
// socket = SSLSocketFactory.getDefault().createSocket();
socket = new Socket();
this.port = port;
this.tokenA = tokenA;
this.tokenB = tokenB;
}
public IPCConnection connect() throws IOException {
InetSocketAddress socketAddress = new InetSocketAddress(SERVER_ADDRESS, port);
Log.i(TAG, "connecting to " + socketAddress);
socket.connect(socketAddress);
return new AppIPCConnection(socket, tokenA, tokenB);
}
}
+1
View File
@@ -22,3 +22,4 @@ dependencyResolutionManagement {
rootProject.name = "Geargrinder"
include(":app")
include(":privd")
include(":libprivd")