Merge "Implement BlobStoreManagerService."
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
index 6aca4a1..60c3136 100644
--- a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
+++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
@@ -22,6 +22,9 @@
import com.android.internal.util.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
+
/**
* An identifier to represent a blob.
*/
@@ -173,6 +176,27 @@
dest.writeString(tag);
}
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof BlobHandle)) {
+ return false;
+ }
+ final BlobHandle other = (BlobHandle) obj;
+ return this.algorithm.equals(other.algorithm)
+ && Arrays.equals(this.digest, other.digest)
+ && this.label.equals(other.label)
+ && this.expiryTimeMillis == other.expiryTimeMillis
+ && this.tag.equals(tag);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(algorithm, Arrays.hashCode(digest), label, expiryTimeMillis, tag);
+ }
+
public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
@Override
public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
index 4395e5a..47af7c0 100644
--- a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
+++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
@@ -45,6 +45,11 @@
*/
@SystemService(Context.BLOB_STORE_SERVICE)
public class BlobStoreManager {
+ /** @hide */
+ public static final int COMMIT_RESULT_SUCCESS = 0;
+ /** @hide */
+ public static final int COMMIT_RESULT_ERROR = 1;
+
private final Context mContext;
private final IBlobStoreManager mService;
@@ -102,7 +107,28 @@
*/
public @NonNull Session openSession(@IntRange(from = 1) long sessionId) throws IOException {
try {
- return new Session(mService.openSession(sessionId));
+ return new Session(mService.openSession(sessionId, mContext.getOpPackageName()));
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Delete an existing session and any data that was written to that session so far.
+ *
+ * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that
+ * represents a particular session.
+ *
+ * @throws IOException when there is an I/O error while deleting the session.
+ * @throws SecurityException when the caller does not own the session, or
+ * the session does not exist or is invalid.
+ */
+ public void deleteSession(@IntRange(from = 1) long sessionId) throws IOException {
+ try {
+ mService.deleteSession(sessionId, mContext.getOpPackageName());
} catch (ParcelableException e) {
e.maybeRethrow(IOException.class);
throw new RuntimeException(e);
@@ -142,6 +168,9 @@
* <p> Any active leases will be automatically released when the blob's expiry time
* ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
*
+ * <p> This lease information is persisted and calling this more than once will result in
+ * latest lease overriding any previous lease.
+ *
* @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
* acquire a lease for.
* @param descriptionResId the resource id for a short description string that can be surfaced
@@ -190,6 +219,9 @@
* <p> Any active leases will be automatically released when the blob's expiry time
* ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
*
+ * <p> This lease information is persisted and calling this more than once will result in
+ * latest lease overriding any previous lease.
+ *
* @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
* acquire a lease for.
* @param descriptionResId the resource id for a short description string that can be surfaced
@@ -279,7 +311,9 @@
public @NonNull ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
@BytesLong long lengthBytes) throws IOException {
try {
- return mSession.openWrite(offsetBytes, lengthBytes);
+ final ParcelFileDescriptor pfd = mSession.openWrite(offsetBytes, lengthBytes);
+ pfd.seekTo(offsetBytes);
+ return pfd;
} catch (ParcelableException e) {
e.maybeRethrow(IOException.class);
throw new RuntimeException(e);
@@ -376,6 +410,31 @@
}
/**
+ * Returns {@code true} if access has been allowed for a {@code packageName} using either
+ * {@link #allowPackageAccess(String, byte[])}.
+ * Otherwise, {@code false}.
+ *
+ * @param packageName the name of the package to check the access for.
+ * @param certificate the input bytes representing a certificate of type
+ * {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+ *
+ * @throws IOException when there is an I/O error while getting the access type.
+ * @throws IllegalStateException when the caller tries to get access type from a session
+ * which is closed or abandoned.
+ */
+ public boolean isPackageAccessAllowed(@NonNull String packageName,
+ @NonNull byte[] certificate) throws IOException {
+ try {
+ return mSession.isPackageAccessAllowed(packageName, certificate);
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Allow packages which are signed with the same certificate as the caller to access this
* blob data once it is committed using a {@link BlobHandle} representing the blob.
*
@@ -399,6 +458,26 @@
}
/**
+ * Returns {@code true} if access has been allowed for packages signed with the same
+ * certificate as the caller by using {@link #allowSameSignatureAccess()}.
+ * Otherwise, {@code false}.
+ *
+ * @throws IOException when there is an I/O error while getting the access type.
+ * @throws IllegalStateException when the caller tries to get access type from a session
+ * which is closed or abandoned.
+ */
+ public boolean isSameSignatureAccessAllowed() throws IOException {
+ try {
+ return mSession.isSameSignatureAccessAllowed();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Allow any app on the device to access this blob data once it is committed using
* a {@link BlobHandle} representing the blob.
*
@@ -427,6 +506,25 @@
}
/**
+ * Returns {@code true} if public access has been allowed by using
+ * {@link #allowPublicAccess()}. Otherwise, {@code false}.
+ *
+ * @throws IOException when there is an I/O error while getting the access type.
+ * @throws IllegalStateException when the caller tries to get access type from a session
+ * which is closed or abandoned.
+ */
+ public boolean isPublicAccessAllowed() throws IOException {
+ try {
+ return mSession.isPublicAccessAllowed();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Commit the file that was written so far to this session to the blob store maintained by
* the system.
*
@@ -439,6 +537,10 @@
* {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)} BlobHandle}
* associated with this session.
*
+ * <p> Committing the same data more than once will result in replacing the corresponding
+ * access mode (via calling one of {@link #allowPackageAccess(String, byte[])},
+ * {@link #allowSameSignatureAccess()}, etc) with the latest one.
+ *
* @param executor the executor on which result callback will be invoked.
* @param resultCallback a callback to receive the commit result. when the result is
* {@code 0}, it indicates success. Otherwise, failure.
diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
index b7a2f1a..dfbf78f 100644
--- a/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
@@ -21,8 +21,9 @@
/** {@hide} */
interface IBlobStoreManager {
long createSession(in BlobHandle handle, in String packageName);
- IBlobStoreSession openSession(long sessionId);
+ IBlobStoreSession openSession(long sessionId, in String packageName);
ParcelFileDescriptor openBlob(in BlobHandle handle, in String packageName);
+ void deleteSession(long sessionId, in String packageName);
void acquireLease(in BlobHandle handle, int descriptionResId, long leaseTimeout,
in String packageName);
diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
index bb5ef3b..4ae919b 100644
--- a/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
@@ -26,6 +26,10 @@
void allowSameSignatureAccess();
void allowPublicAccess();
+ boolean isPackageAccessAllowed(in String packageName, in byte[] certificate);
+ boolean isSameSignatureAccessAllowed();
+ boolean isPublicAccessAllowed();
+
long getSize();
void close();
void abandon();
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
new file mode 100644
index 0000000..357250a
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.blob;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.ArraySet;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Class for representing how a blob can be shared.
+ *
+ * Note that this class is not thread-safe, callers need to take of synchronizing access.
+ */
+class BlobAccessMode {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {
+ ACCESS_TYPE_PRIVATE,
+ ACCESS_TYPE_PUBLIC,
+ ACCESS_TYPE_SAME_SIGNATURE,
+ ACCESS_TYPE_WHITELIST,
+ })
+ @interface AccessType {}
+ static final int ACCESS_TYPE_PRIVATE = 1 << 0;
+ static final int ACCESS_TYPE_PUBLIC = 1 << 1;
+ static final int ACCESS_TYPE_SAME_SIGNATURE = 1 << 2;
+ static final int ACCESS_TYPE_WHITELIST = 1 << 3;
+
+ private int mAccessType = ACCESS_TYPE_PRIVATE;
+
+ private final ArraySet<PackageIdentifier> mWhitelistedPackages = new ArraySet<>();
+
+ void allow(BlobAccessMode other) {
+ if ((other.mAccessType & ACCESS_TYPE_WHITELIST) != 0) {
+ mWhitelistedPackages.addAll(other.mWhitelistedPackages);
+ }
+ mAccessType |= other.mAccessType;
+ }
+
+ void allowPublicAccess() {
+ mAccessType |= ACCESS_TYPE_PUBLIC;
+ }
+
+ void allowSameSignatureAccess() {
+ mAccessType |= ACCESS_TYPE_SAME_SIGNATURE;
+ }
+
+ void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) {
+ mAccessType |= ACCESS_TYPE_WHITELIST;
+ mWhitelistedPackages.add(PackageIdentifier.create(packageName, certificate));
+ }
+
+ boolean isPublicAccessAllowed() {
+ return (mAccessType & ACCESS_TYPE_PUBLIC) != 0;
+ }
+
+ boolean isSameSignatureAccessAllowed() {
+ return (mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0;
+ }
+
+ boolean isPackageAccessAllowed(@NonNull String packageName, @NonNull byte[] certificate) {
+ if ((mAccessType & ACCESS_TYPE_WHITELIST) == 0) {
+ return false;
+ }
+ return mWhitelistedPackages.contains(PackageIdentifier.create(packageName, certificate));
+ }
+
+ boolean isAccessAllowedForCaller(Context context,
+ @NonNull String callingPackage, @NonNull String committerPackage) {
+ if ((mAccessType & ACCESS_TYPE_PUBLIC) != 0) {
+ return true;
+ }
+
+ final PackageManager pm = context.getPackageManager();
+ if ((mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0) {
+ if (pm.checkSignatures(committerPackage, callingPackage)
+ == PackageManager.SIGNATURE_MATCH) {
+ return true;
+ }
+ }
+
+ if ((mAccessType & ACCESS_TYPE_WHITELIST) != 0) {
+ for (int i = 0; i < mWhitelistedPackages.size(); ++i) {
+ final PackageIdentifier packageIdentifier = mWhitelistedPackages.valueAt(i);
+ if (packageIdentifier.packageName.equals(callingPackage)
+ && pm.hasSigningCertificate(callingPackage, packageIdentifier.certificate,
+ PackageManager.CERT_INPUT_SHA256)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static final class PackageIdentifier {
+ public final String packageName;
+ public final byte[] certificate;
+
+ private PackageIdentifier(@NonNull String packageName, @NonNull byte[] certificate) {
+ this.packageName = packageName;
+ this.certificate = certificate;
+ }
+
+ public static PackageIdentifier create(@NonNull String packageName,
+ @NonNull byte[] certificate) {
+ return new PackageIdentifier(packageName, certificate);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof PackageIdentifier)) {
+ return false;
+ }
+ final PackageIdentifier other = (PackageIdentifier) obj;
+ return this.packageName.equals(other.packageName)
+ && Arrays.equals(this.certificate, other.certificate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, Arrays.hashCode(certificate));
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
new file mode 100644
index 0000000..d3a22715
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.blob;
+
+import static android.system.OsConstants.O_RDONLY;
+
+import android.annotation.NonNull;
+import android.app.blob.BlobHandle;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RevocableFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+class BlobMetadata {
+ private final Object mMetadataLock = new Object();
+
+ private final Context mContext;
+ private final long mBlobId;
+ private final BlobHandle mBlobHandle;
+
+ @GuardedBy("mMetadataLock")
+ private final ArraySet<Committer> mCommitters = new ArraySet<>();
+
+ @GuardedBy("mMetadataLock")
+ private final ArraySet<Leasee> mLeasees = new ArraySet<>();
+
+ /**
+ * Contains packageName -> {RevocableFileDescriptors}.
+ *
+ * Keep track of RevocableFileDescriptors given to clients which are not yet revoked/closed so
+ * that when clients access is revoked or the blob gets deleted, we can be sure that clients
+ * do not have any reference to the blob and the space occupied by the blob can be freed.
+ */
+ @GuardedBy("mRevocableFds")
+ private final ArrayMap<String, ArraySet<RevocableFileDescriptor>> mRevocableFds =
+ new ArrayMap<>();
+
+ BlobMetadata(Context context, long blobId, BlobHandle blobHandle) {
+ mContext = context;
+ mBlobId = blobId;
+ mBlobHandle = blobHandle;
+ }
+
+ void addCommitter(String packageName, int uid, BlobAccessMode blobAccessMode) {
+ synchronized (mMetadataLock) {
+ mCommitters.add(new Committer(packageName, uid, blobAccessMode));
+ }
+ }
+
+ void addLeasee(String callingPackage, int callingUid,
+ int descriptionResId, long leaseExpiryTimeMillis) {
+ synchronized (mMetadataLock) {
+ mLeasees.add(new Leasee(callingPackage, callingUid,
+ descriptionResId, leaseExpiryTimeMillis));
+ }
+ }
+
+ void removeLeasee(String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ mLeasees.remove(new Accessor(packageName, uid));
+ }
+ }
+
+ boolean isAccessAllowedForCaller(String callingPackage, int callingUid) {
+ // TODO: verify blob is still valid (expiryTime is not elapsed)
+ synchronized (mMetadataLock) {
+ // Check if packageName already holds a lease on the blob.
+ for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+ final Leasee leasee = mLeasees.valueAt(i);
+ if (leasee.equals(callingPackage, callingUid)
+ && leasee.isStillValid()) {
+ return true;
+ }
+ }
+
+ for (int i = 0, size = mCommitters.size(); i < size; ++i) {
+ final Committer committer = mCommitters.valueAt(i);
+
+ // Check if the caller is the same package that committed the blob.
+ if (committer.equals(callingPackage, callingUid)) {
+ return true;
+ }
+
+ // Check if the caller is allowed access as per the access mode specified
+ // by the committer.
+ if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
+ callingPackage, committer.packageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ ParcelFileDescriptor openForRead(String callingPackage) throws IOException {
+ // TODO: Add limit on opened fds
+ FileDescriptor fd;
+ try {
+ fd = Os.open(BlobStoreConfig.getBlobFile(mBlobId).getPath(), O_RDONLY, 0);
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ synchronized (mMetadataLock) {
+ return createRevocableFdLocked(fd, callingPackage);
+ }
+ }
+
+ @GuardedBy("mMetadataLock")
+ @NonNull
+ private ParcelFileDescriptor createRevocableFdLocked(FileDescriptor fd,
+ String callingPackage) throws IOException {
+ final RevocableFileDescriptor revocableFd =
+ new RevocableFileDescriptor(mContext, fd);
+ synchronized (mRevocableFds) {
+ ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
+ mRevocableFds.get(callingPackage);
+ if (revocableFdsForPkg == null) {
+ revocableFdsForPkg = new ArraySet<>();
+ mRevocableFds.put(callingPackage, revocableFdsForPkg);
+ }
+ revocableFdsForPkg.add(revocableFd);
+ }
+ revocableFd.addOnCloseListener((e) -> {
+ synchronized (mRevocableFds) {
+ final ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
+ mRevocableFds.get(callingPackage);
+ if (revocableFdsForPkg != null) {
+ revocableFdsForPkg.remove(revocableFd);
+ }
+ }
+ });
+ return revocableFd.getRevocableFileDescriptor();
+ }
+
+ static final class Committer extends Accessor {
+ public final BlobAccessMode blobAccessMode;
+
+ Committer(String packageName, int uid, BlobAccessMode blobAccessMode) {
+ super(packageName, uid);
+ this.blobAccessMode = blobAccessMode;
+ }
+ }
+
+ static final class Leasee extends Accessor {
+ public final int descriptionResId;
+ public final long expiryTimeMillis;
+
+ Leasee(String packageName, int uid, int descriptionResId, long expiryTimeMillis) {
+ super(packageName, uid);
+ this.descriptionResId = descriptionResId;
+ this.expiryTimeMillis = expiryTimeMillis;
+ }
+
+ boolean isStillValid() {
+ return expiryTimeMillis == 0 || expiryTimeMillis <= System.currentTimeMillis();
+ }
+ }
+
+ static class Accessor {
+ public final String packageName;
+ public final int uid;
+
+ Accessor(String packageName, int uid) {
+ this.packageName = packageName;
+ this.uid = uid;
+ }
+
+ public boolean equals(String packageName, int uid) {
+ return this.uid == uid && this.packageName.equals(packageName);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof Accessor)) {
+ return false;
+ }
+ final Accessor other = (Accessor) obj;
+ return this.uid == other.uid && this.packageName.equals(other.packageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, uid);
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
new file mode 100644
index 0000000..b9a4b17
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.blob;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Environment;
+import android.util.Slog;
+
+import java.io.File;
+
+class BlobStoreConfig {
+ public static final String TAG = "BlobStore";
+
+ @Nullable
+ public static File prepareBlobFile(long sessionId) {
+ final File blobsDir = prepareBlobsDir();
+ return blobsDir == null ? null : getBlobFile(blobsDir, sessionId);
+ }
+
+ @NonNull
+ public static File getBlobFile(long sessionId) {
+ return getBlobFile(getBlobsDir(), sessionId);
+ }
+
+ @NonNull
+ private static File getBlobFile(File blobsDir, long sessionId) {
+ return new File(blobsDir, String.valueOf(sessionId));
+ }
+
+ @Nullable
+ public static File prepareBlobsDir() {
+ final File blobsDir = getBlobsDir(prepareBlobStoreRootDir());
+ if (!blobsDir.exists() && !blobsDir.mkdir()) {
+ Slog.e(TAG, "Failed to mkdir(): " + blobsDir);
+ return null;
+ }
+ return blobsDir;
+ }
+
+ @NonNull
+ public static File getBlobsDir() {
+ return getBlobsDir(getBlobStoreRootDir());
+ }
+
+ @NonNull
+ private static File getBlobsDir(File blobsRootDir) {
+ return new File(blobsRootDir, "blobs");
+ }
+
+ @Nullable
+ public static File prepareBlobStoreRootDir() {
+ final File blobStoreRootDir = getBlobStoreRootDir();
+ if (!blobStoreRootDir.exists() && !blobStoreRootDir.mkdir()) {
+ Slog.e(TAG, "Failed to mkdir(): " + blobStoreRootDir);
+ return null;
+ }
+ return blobStoreRootDir;
+ }
+
+ @NonNull
+ public static File getBlobStoreRootDir() {
+ return new File(Environment.getDataSystemDirectory(), "blobstore");
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index b204fee..9d60f86 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -15,58 +15,350 @@
*/
package com.android.server.blob;
+import static android.app.blob.BlobStoreManager.COMMIT_RESULT_SUCCESS;
+
+import static com.android.server.blob.BlobStoreConfig.TAG;
+import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
+import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
+import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID;
+import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_VALID;
+import static com.android.server.blob.BlobStoreSession.stateToString;
+
import android.annotation.CurrentTimeSecondsLong;
import android.annotation.IdRes;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.app.blob.BlobHandle;
import android.app.blob.IBlobStoreManager;
import android.app.blob.IBlobStoreSession;
import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ExceptionUtils;
+import android.util.LongSparseArray;
+import android.util.Slog;
+import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
import com.android.server.SystemService;
+import com.android.server.Watchdog;
+
+import java.io.IOException;
/**
* Service responsible for maintaining and facilitating access to data blobs published by apps.
*/
public class BlobStoreManagerService extends SystemService {
+ private final Object mBlobsLock = new Object();
+
+ // Contains data of userId -> {sessionId -> {BlobStoreSession}}.
+ @GuardedBy("mBlobsLock")
+ private final SparseArray<LongSparseArray<BlobStoreSession>> mSessions = new SparseArray<>();
+
+ @GuardedBy("mBlobsLock")
+ private long mCurrentMaxSessionId;
+
+ // Contains data of userId -> {BlobHandle -> {BlobMetadata}}
+ @GuardedBy("mBlobsLock")
+ private final SparseArray<ArrayMap<BlobHandle, BlobMetadata>> mBlobsMap = new SparseArray<>();
+
private final Context mContext;
+ private final Handler mHandler;
+ private final SessionStateChangeListener mSessionStateChangeListener =
+ new SessionStateChangeListener();
+
+ private PackageManagerInternal mPackageManagerInternal;
public BlobStoreManagerService(Context context) {
super(context);
mContext = context;
+
+ final HandlerThread handlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_BACKGROUND, true /* allowIo */);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ Watchdog.getInstance().addThread(mHandler);
}
@Override
public void onStart() {
publishBinderService(Context.BLOB_STORE_SERVICE, new Stub());
+
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ }
+
+
+ @GuardedBy("mBlobsLock")
+ private long generateNextSessionIdLocked() {
+ return ++mCurrentMaxSessionId;
+ }
+
+ @GuardedBy("mBlobsLock")
+ private LongSparseArray<BlobStoreSession> getUserSessionsLocked(int userId) {
+ LongSparseArray<BlobStoreSession> userSessions = mSessions.get(userId);
+ if (userSessions == null) {
+ userSessions = new LongSparseArray<>();
+ mSessions.put(userId, userSessions);
+ }
+ return userSessions;
+ }
+
+ @GuardedBy("mBlobsLock")
+ private ArrayMap<BlobHandle, BlobMetadata> getUserBlobsLocked(int userId) {
+ ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.get(userId);
+ if (userBlobs == null) {
+ userBlobs = new ArrayMap<>();
+ mBlobsMap.put(userId, userBlobs);
+ }
+ return userBlobs;
+ }
+
+ private long createSessionInternal(BlobHandle blobHandle,
+ int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ // TODO: throw if there is already an active session associated with blobHandle.
+ final long sessionId = generateNextSessionIdLocked();
+ final BlobStoreSession session = new BlobStoreSession(mContext,
+ sessionId, blobHandle, callingUid, callingPackage,
+ mSessionStateChangeListener);
+ getUserSessionsLocked(UserHandle.getUserId(callingUid)).put(sessionId, session);
+ // TODO: persist sessions data
+ return sessionId;
+ }
+ }
+
+ private BlobStoreSession openSessionInternal(long sessionId,
+ int callingUid, String callingPackage) {
+ final BlobStoreSession session;
+ synchronized (mBlobsLock) {
+ session = getUserSessionsLocked(
+ UserHandle.getUserId(callingUid)).get(sessionId);
+ if (session == null || !session.hasAccess(callingUid, callingPackage)
+ || session.isFinalized()) {
+ throw new SecurityException("Session not found: " + sessionId);
+ }
+ }
+ session.open();
+ return session;
+ }
+
+ private void deleteSessionInternal(long sessionId,
+ int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ final BlobStoreSession session = openSessionInternal(sessionId,
+ callingUid, callingPackage);
+ session.open();
+ session.abandon();
+ // TODO: persist sessions data
+ }
+ }
+
+ private ParcelFileDescriptor openBlobInternal(BlobHandle blobHandle, int callingUid,
+ String callingPackage) throws IOException {
+ synchronized (mBlobsLock) {
+ final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+ .get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ return blobMetadata.openForRead(callingPackage);
+ }
+ }
+
+ private void acquireLeaseInternal(BlobHandle blobHandle, int descriptionResId,
+ long leaseExpiryTimeMillis, int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+ .get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ if (leaseExpiryTimeMillis != 0 && leaseExpiryTimeMillis > blobHandle.expiryTimeMillis) {
+ throw new IllegalArgumentException(
+ "Lease expiry cannot be later than blobs expiry time");
+ }
+ blobMetadata.addLeasee(callingPackage, callingUid,
+ descriptionResId, leaseExpiryTimeMillis);
+ // TODO: persist blobs data
+ }
+ }
+
+ private void releaseLeaseInternal(BlobHandle blobHandle, int callingUid,
+ String callingPackage) {
+ synchronized (mBlobsLock) {
+ final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+ .get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ blobMetadata.removeLeasee(callingPackage, callingUid);
+ }
+ }
+
+ private void verifyCallingPackage(int callingUid, String callingPackage) {
+ if (mPackageManagerInternal.getPackageUid(
+ callingPackage, 0, UserHandle.getUserId(callingUid)) != callingUid) {
+ throw new SecurityException("Specified calling package [" + callingPackage
+ + "] does not match the calling uid " + callingUid);
+ }
+ }
+
+ class SessionStateChangeListener {
+ public void onStateChanged(@NonNull BlobStoreSession session) {
+ mHandler.post(PooledLambda.obtainRunnable(
+ BlobStoreManagerService::onStateChangedInternal,
+ BlobStoreManagerService.this, session));
+ }
+ }
+
+ private void onStateChangedInternal(@NonNull BlobStoreSession session) {
+ synchronized (mBlobsLock) {
+ switch (session.getState()) {
+ case STATE_ABANDONED:
+ case STATE_VERIFIED_INVALID:
+ session.getSessionFile().delete();
+ getUserSessionsLocked(UserHandle.getUserId(session.ownerUid))
+ .remove(session.sessionId);
+ break;
+ case STATE_COMMITTED:
+ session.verifyBlobData();
+ break;
+ case STATE_VERIFIED_VALID:
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs =
+ getUserBlobsLocked(UserHandle.getUserId(session.ownerUid));
+ BlobMetadata blob = userBlobs.get(session.blobHandle);
+ if (blob == null) {
+ blob = new BlobMetadata(mContext,
+ session.sessionId, session.blobHandle);
+ userBlobs.put(session.blobHandle, blob);
+ }
+ blob.addCommitter(session.ownerPackageName, session.ownerUid,
+ session.getBlobAccessMode());
+ // TODO: Persist blobs data.
+ session.sendCommitCallbackResult(COMMIT_RESULT_SUCCESS);
+ getUserSessionsLocked(UserHandle.getUserId(session.ownerUid))
+ .remove(session.sessionId);
+ break;
+ default:
+ Slog.wtf(TAG, "Invalid session state: "
+ + stateToString(session.getState()));
+ }
+ // TODO: Persist sessions data.
+ }
}
private class Stub extends IBlobStoreManager.Stub {
@Override
- public long createSession(@NonNull BlobHandle blobHandle, @NonNull String packageName) {
- return 0;
+ @IntRange(from = 1)
+ public long createSession(@NonNull BlobHandle blobHandle,
+ @NonNull String packageName) {
+ Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+ // TODO: verify blobHandle.algorithm is sha-256
+ // TODO: assert blobHandle is valid.
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to create session; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ // TODO: Verify caller request is within limits (no. of calls/blob sessions/blobs)
+ return createSessionInternal(blobHandle, callingUid, packageName);
}
@Override
- public IBlobStoreSession openSession(long sessionId) {
- return null;
+ @NonNull
+ public IBlobStoreSession openSession(@IntRange(from = 1) long sessionId,
+ @NonNull String packageName) {
+ Preconditions.checkArgumentPositive(sessionId,
+ "sessionId must be positive: " + sessionId);
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ return openSessionInternal(sessionId, callingUid, packageName);
+ }
+
+ @Override
+ public void deleteSession(@IntRange(from = 1) long sessionId,
+ @NonNull String packageName) {
+ Preconditions.checkArgumentPositive(sessionId,
+ "sessionId must be positive: " + sessionId);
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ deleteSessionInternal(sessionId, callingUid, packageName);
}
@Override
public ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle,
@NonNull String packageName) {
- return null;
+ Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to open blob; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ try {
+ return openBlobInternal(blobHandle, callingUid, packageName);
+ } catch (IOException e) {
+ throw ExceptionUtils.wrap(e);
+ }
}
@Override
public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId,
- @CurrentTimeSecondsLong long leaseTimeout, @NonNull String packageName) {
+ @CurrentTimeSecondsLong long leaseTimeoutSecs, @NonNull String packageName) {
+ Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ acquireLeaseInternal(blobHandle, descriptionResId, leaseTimeoutSecs,
+ callingUid, packageName);
}
@Override
public void releaseLease(@NonNull BlobHandle blobHandle, @NonNull String packageName) {
+ Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ releaseLeaseInternal(blobHandle, callingUid, packageName);
}
}
}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
index 2c38e76..29092b3 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
@@ -15,20 +15,175 @@
*/
package com.android.server.blob;
+import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR;
+import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_RDWR;
+import static android.system.OsConstants.SEEK_SET;
+
+import static com.android.server.blob.BlobStoreConfig.TAG;
+
import android.annotation.BytesLong;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.blob.BlobHandle;
import android.app.blob.IBlobCommitCallback;
import android.app.blob.IBlobStoreSession;
+import android.content.Context;
+import android.os.Binder;
+import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.RevocableFileDescriptor;
+import android.os.storage.StorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ExceptionUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
/** TODO: add doc */
public class BlobStoreSession extends IBlobStoreSession.Stub {
+ static final int STATE_OPENED = 1;
+ static final int STATE_CLOSED = 0;
+ static final int STATE_ABANDONED = 2;
+ static final int STATE_COMMITTED = 3;
+ static final int STATE_VERIFIED_VALID = 4;
+ static final int STATE_VERIFIED_INVALID = 5;
+
+ private final Object mSessionLock = new Object();
+
+ private final Context mContext;
+ private final SessionStateChangeListener mListener;
+
+ public final BlobHandle blobHandle;
+ public final long sessionId;
+ public final int ownerUid;
+ public final String ownerPackageName;
+
+ // Do not access this directly, instead use getSessionFile().
+ private File mSessionFile;
+
+ @GuardedBy("mRevocableFds")
+ private ArrayList<RevocableFileDescriptor> mRevocableFds = new ArrayList<>();
+
+ @GuardedBy("mSessionLock")
+ private int mState = STATE_CLOSED;
+
+ @GuardedBy("mSessionLock")
+ private final BlobAccessMode mBlobAccessMode = new BlobAccessMode();
+
+ @GuardedBy("mSessionLock")
+ private IBlobCommitCallback mBlobCommitCallback;
+
+ BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle,
+ int ownerUid, String ownerPackageName, SessionStateChangeListener listener) {
+ this.mContext = context;
+ this.blobHandle = blobHandle;
+ this.sessionId = sessionId;
+ this.ownerUid = ownerUid;
+ this.ownerPackageName = ownerPackageName;
+ this.mListener = listener;
+ }
+
+ boolean hasAccess(int callingUid, String callingPackageName) {
+ return ownerUid == callingUid && ownerPackageName.equals(callingPackageName);
+ }
+
+ void open() {
+ synchronized (mSessionLock) {
+ if (isFinalized()) {
+ throw new IllegalStateException("Not allowed to open session with state: "
+ + stateToString(mState));
+ }
+ mState = STATE_OPENED;
+ }
+ }
+
+ int getState() {
+ synchronized (mSessionLock) {
+ return mState;
+ }
+ }
+
+ void sendCommitCallbackResult(int result) {
+ synchronized (mSessionLock) {
+ try {
+ mBlobCommitCallback.onResult(result);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Error sending the callback result", e);
+ }
+ mBlobCommitCallback = null;
+ }
+ }
+
+ BlobAccessMode getBlobAccessMode() {
+ synchronized (mSessionLock) {
+ return mBlobAccessMode;
+ }
+ }
+
+ boolean isFinalized() {
+ synchronized (mSessionLock) {
+ return mState == STATE_COMMITTED || mState == STATE_ABANDONED;
+ }
+ }
+
@Override
@NonNull
public ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
@BytesLong long lengthBytes) {
- return null;
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to write in state: "
+ + stateToString(mState));
+ }
+
+ try {
+ return openWriteLocked(offsetBytes, lengthBytes);
+ } catch (IOException e) {
+ throw ExceptionUtils.wrap(e);
+ }
+ }
+ }
+
+ @GuardedBy("mSessionLock")
+ @NonNull
+ private ParcelFileDescriptor openWriteLocked(@BytesLong long offsetBytes,
+ @BytesLong long lengthBytes) throws IOException {
+ // TODO: Add limit on active open sessions/writes/reads
+ FileDescriptor fd = null;
+ try {
+ final File sessionFile = getSessionFile();
+ if (sessionFile == null) {
+ throw new IllegalStateException("Couldn't get the file for this session");
+ }
+ fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600);
+ if (offsetBytes > 0) {
+ final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET);
+ if (curOffset != offsetBytes) {
+ throw new IllegalStateException("Failed to seek " + offsetBytes
+ + "; curOffset=" + offsetBytes);
+ }
+ }
+ if (lengthBytes > 0) {
+ mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes);
+ }
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+ return createRevocableFdLocked(fd);
}
@Override
@@ -40,25 +195,192 @@
@Override
public void allowPackageAccess(@NonNull String packageName,
@NonNull byte[] certificate) {
+ assertCallerIsOwner();
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to change access type in state: "
+ + stateToString(mState));
+ }
+ mBlobAccessMode.allowPackageAccess(packageName, certificate);
+ }
}
@Override
public void allowSameSignatureAccess() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to change access type in state: "
+ + stateToString(mState));
+ }
+ mBlobAccessMode.allowSameSignatureAccess();
+ }
}
@Override
public void allowPublicAccess() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to change access type in state: "
+ + stateToString(mState));
+ }
+ mBlobAccessMode.allowPublicAccess();
+ }
+ }
+
+ @Override
+ public boolean isPackageAccessAllowed(@NonNull String packageName,
+ @NonNull byte[] certificate) {
+ assertCallerIsOwner();
+ Preconditions.checkNotNull(packageName, "packageName must not be null");
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to get access type in state: "
+ + stateToString(mState));
+ }
+ return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate);
+ }
+ }
+
+ @Override
+ public boolean isSameSignatureAccessAllowed() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to get access type in state: "
+ + stateToString(mState));
+ }
+ return mBlobAccessMode.isSameSignatureAccessAllowed();
+ }
+ }
+
+ @Override
+ public boolean isPublicAccessAllowed() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to get access type in state: "
+ + stateToString(mState));
+ }
+ return mBlobAccessMode.isPublicAccessAllowed();
+ }
}
@Override
public void close() {
+ closeSession(STATE_CLOSED);
}
@Override
public void abandon() {
+ closeSession(STATE_ABANDONED);
}
@Override
public void commit(IBlobCommitCallback callback) {
+ synchronized (mSessionLock) {
+ mBlobCommitCallback = callback;
+
+ closeSession(STATE_COMMITTED);
+ }
+ }
+
+ private void closeSession(int state) {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ if (state == STATE_CLOSED) {
+ // Just trying to close the session which is already deleted or abandoned,
+ // ignore.
+ return;
+ } else {
+ throw new IllegalStateException("Not allowed to delete or abandon a session"
+ + " with state: " + stateToString(mState));
+ }
+ }
+
+ mState = state;
+ revokeAllFdsLocked();
+
+ mListener.onStateChanged(this);
+ }
+ }
+
+ void verifyBlobData() {
+ byte[] actualDigest = null;
+ try {
+ actualDigest = FileUtils.digest(getSessionFile(), blobHandle.algorithm);
+ } catch (IOException | NoSuchAlgorithmException e) {
+ Slog.e(TAG, "Error computing the digest", e);
+ }
+ synchronized (mSessionLock) {
+ if (actualDigest != null && Arrays.equals(actualDigest, blobHandle.digest)) {
+ mState = STATE_VERIFIED_VALID;
+ // Commit callback will be sent once the data is persisted.
+ } else {
+ mState = STATE_VERIFIED_INVALID;
+ sendCommitCallbackResult(COMMIT_RESULT_ERROR);
+ }
+ mListener.onStateChanged(this);
+ }
+ }
+
+ @GuardedBy("mSessionLock")
+ private void revokeAllFdsLocked() {
+ for (int i = mRevocableFds.size() - 1; i >= 0; --i) {
+ mRevocableFds.get(i).revoke();
+ mRevocableFds.remove(i);
+ }
+ }
+
+ @GuardedBy("mSessionLock")
+ @NonNull
+ private ParcelFileDescriptor createRevocableFdLocked(FileDescriptor fd)
+ throws IOException {
+ final RevocableFileDescriptor revocableFd =
+ new RevocableFileDescriptor(mContext, fd);
+ synchronized (mRevocableFds) {
+ mRevocableFds.add(revocableFd);
+ }
+ revocableFd.addOnCloseListener((e) -> {
+ synchronized (mRevocableFds) {
+ mRevocableFds.remove(revocableFd);
+ }
+ });
+ return revocableFd.getRevocableFileDescriptor();
+ }
+
+ @Nullable
+ File getSessionFile() {
+ if (mSessionFile == null) {
+ mSessionFile = BlobStoreConfig.prepareBlobFile(sessionId);
+ }
+ return mSessionFile;
+ }
+
+ @NonNull
+ static String stateToString(int state) {
+ switch (state) {
+ case STATE_OPENED:
+ return "<opened>";
+ case STATE_CLOSED:
+ return "<closed>";
+ case STATE_ABANDONED:
+ return "<abandoned>";
+ case STATE_COMMITTED:
+ return "<committed>";
+ default:
+ Slog.wtf(TAG, "Unknown state: " + state);
+ return "<unknown>";
+ }
+ }
+
+ private void assertCallerIsOwner() {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != ownerUid) {
+ throw new SecurityException(ownerUid + " is not the session owner");
+ }
}
}
diff --git a/api/current.txt b/api/current.txt
index d70e6ca..be0936a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -7511,6 +7511,7 @@
method public void acquireLease(@NonNull android.app.blob.BlobHandle, @IdRes int, long) throws java.io.IOException;
method public void acquireLease(@NonNull android.app.blob.BlobHandle, @IdRes int) throws java.io.IOException;
method @IntRange(from=1) public long createSession(@NonNull android.app.blob.BlobHandle) throws java.io.IOException;
+ method public void deleteSession(@IntRange(from=1) long) throws java.io.IOException;
method @NonNull public android.os.ParcelFileDescriptor openBlob(@NonNull android.app.blob.BlobHandle) throws java.io.IOException;
method @NonNull public android.app.blob.BlobStoreManager.Session openSession(@IntRange(from=1) long) throws java.io.IOException;
method public void releaseLease(@NonNull android.app.blob.BlobHandle) throws java.io.IOException;
@@ -7524,6 +7525,9 @@
method public void close() throws java.io.IOException;
method public void commit(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>) throws java.io.IOException;
method public long getSize() throws java.io.IOException;
+ method public boolean isPackageAccessAllowed(@NonNull String, @NonNull byte[]) throws java.io.IOException;
+ method public boolean isPublicAccessAllowed() throws java.io.IOException;
+ method public boolean isSameSignatureAccessAllowed() throws java.io.IOException;
method @NonNull public android.os.ParcelFileDescriptor openWrite(long, long) throws java.io.IOException;
}
diff --git a/core/java/android/os/RevocableFileDescriptor.java b/core/java/android/os/RevocableFileDescriptor.java
index a750ce6..ac2cd60 100644
--- a/core/java/android/os/RevocableFileDescriptor.java
+++ b/core/java/android/os/RevocableFileDescriptor.java
@@ -48,6 +48,8 @@
private volatile boolean mRevoked;
+ private ParcelFileDescriptor.OnCloseListener mOnCloseListener;
+
/** {@hide} */
public RevocableFileDescriptor() {
}
@@ -97,6 +99,14 @@
IoUtils.closeQuietly(mInner);
}
+ /**
+ * Callback for indicating that {@link ParcelFileDescriptor} passed to the client
+ * process ({@link #getRevocableFileDescriptor()}) has been closed.
+ */
+ public void addOnCloseListener(ParcelFileDescriptor.OnCloseListener onCloseListener) {
+ mOnCloseListener = onCloseListener;
+ }
+
public boolean isRevoked() {
return mRevoked;
}
@@ -156,6 +166,9 @@
if (DEBUG) Slog.v(TAG, "onRelease()");
mRevoked = true;
IoUtils.closeQuietly(mInner);
+ if (mOnCloseListener != null) {
+ mOnCloseListener.onClose(null);
+ }
}
};
}