| /* |
| * 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.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR; |
| import static android.app.blob.XmlTags.ATTR_ID; |
| import static android.app.blob.XmlTags.ATTR_PACKAGE; |
| import static android.app.blob.XmlTags.ATTR_UID; |
| import static android.app.blob.XmlTags.TAG_ACCESS_MODE; |
| import static android.app.blob.XmlTags.TAG_BLOB_HANDLE; |
| import static android.system.OsConstants.O_CREAT; |
| import static android.system.OsConstants.O_RDONLY; |
| 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.annotations.VisibleForTesting; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.internal.util.Preconditions; |
| import com.android.internal.util.XmlUtils; |
| import com.android.server.blob.BlobStoreManagerService.DumpArgs; |
| import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Objects; |
| |
| /** TODO: add doc */ |
| @VisibleForTesting |
| 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; |
| |
| private final BlobHandle mBlobHandle; |
| private final long mSessionId; |
| private final int mOwnerUid; |
| private final String mOwnerPackageName; |
| |
| // 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.mBlobHandle = blobHandle; |
| this.mSessionId = sessionId; |
| this.mOwnerUid = ownerUid; |
| this.mOwnerPackageName = ownerPackageName; |
| this.mListener = listener; |
| } |
| |
| public BlobHandle getBlobHandle() { |
| return mBlobHandle; |
| } |
| |
| public long getSessionId() { |
| return mSessionId; |
| } |
| |
| public int getOwnerUid() { |
| return mOwnerUid; |
| } |
| |
| public String getOwnerPackageName() { |
| return mOwnerPackageName; |
| } |
| |
| boolean hasAccess(int callingUid, String callingPackageName) { |
| return mOwnerUid == callingUid && mOwnerPackageName.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) { |
| Preconditions.checkArgumentNonnegative(offsetBytes, "offsetBytes must not be negative"); |
| |
| 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 |
| @NonNull |
| public ParcelFileDescriptor openRead() { |
| assertCallerIsOwner(); |
| synchronized (mSessionLock) { |
| if (mState != STATE_OPENED) { |
| throw new IllegalStateException("Not allowed to read in state: " |
| + stateToString(mState)); |
| } |
| |
| try { |
| return openReadLocked(); |
| } catch (IOException e) { |
| throw ExceptionUtils.wrap(e); |
| } |
| } |
| } |
| |
| @GuardedBy("mSessionLock") |
| @NonNull |
| private ParcelFileDescriptor openReadLocked() throws IOException { |
| 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_RDONLY, 0); |
| } catch (ErrnoException e) { |
| e.rethrowAsIOException(); |
| } |
| return createRevocableFdLocked(fd); |
| } |
| |
| @Override |
| @BytesLong |
| public long getSize() { |
| return getSessionFile().length(); |
| } |
| |
| @Override |
| public void allowPackageAccess(@NonNull String packageName, |
| @NonNull byte[] certificate) { |
| assertCallerIsOwner(); |
| Objects.requireNonNull(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(); |
| Objects.requireNonNull(packageName, "packageName must not be null"); |
| Preconditions.checkByteArrayNotEmpty(certificate, "certificate"); |
| |
| 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(), mBlobHandle.algorithm); |
| } catch (IOException | NoSuchAlgorithmException e) { |
| Slog.e(TAG, "Error computing the digest", e); |
| } |
| synchronized (mSessionLock) { |
| if (actualDigest != null && Arrays.equals(actualDigest, mBlobHandle.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.clear(); |
| } |
| |
| @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(mSessionId); |
| } |
| 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 != mOwnerUid) { |
| throw new SecurityException(mOwnerUid + " is not the session owner"); |
| } |
| } |
| |
| void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) { |
| synchronized (mSessionLock) { |
| fout.println("state: " + stateToString(mState)); |
| fout.println("ownerUid: " + mOwnerUid); |
| fout.println("ownerPkg: " + mOwnerPackageName); |
| |
| fout.println("blobHandle:"); |
| fout.increaseIndent(); |
| mBlobHandle.dump(fout, dumpArgs.shouldDumpFull()); |
| fout.decreaseIndent(); |
| |
| fout.println("accessMode:"); |
| fout.increaseIndent(); |
| mBlobAccessMode.dump(fout); |
| fout.decreaseIndent(); |
| |
| fout.println("Open fds: #" + mRevocableFds.size()); |
| } |
| } |
| |
| void writeToXml(@NonNull XmlSerializer out) throws IOException { |
| synchronized (mSessionLock) { |
| XmlUtils.writeLongAttribute(out, ATTR_ID, mSessionId); |
| XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, mOwnerPackageName); |
| XmlUtils.writeIntAttribute(out, ATTR_UID, mOwnerUid); |
| |
| out.startTag(null, TAG_BLOB_HANDLE); |
| mBlobHandle.writeToXml(out); |
| out.endTag(null, TAG_BLOB_HANDLE); |
| |
| out.startTag(null, TAG_ACCESS_MODE); |
| mBlobAccessMode.writeToXml(out); |
| out.endTag(null, TAG_ACCESS_MODE); |
| } |
| } |
| |
| @Nullable |
| static BlobStoreSession createFromXml(@NonNull XmlPullParser in, |
| @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener) |
| throws IOException, XmlPullParserException { |
| final int sessionId = XmlUtils.readIntAttribute(in, ATTR_ID); |
| final String ownerPackageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); |
| final int ownerUid = XmlUtils.readIntAttribute(in, ATTR_UID); |
| |
| final int depth = in.getDepth(); |
| BlobHandle blobHandle = null; |
| BlobAccessMode blobAccessMode = null; |
| while (XmlUtils.nextElementWithin(in, depth)) { |
| if (TAG_BLOB_HANDLE.equals(in.getName())) { |
| blobHandle = BlobHandle.createFromXml(in); |
| } else if (TAG_ACCESS_MODE.equals(in.getName())) { |
| blobAccessMode = BlobAccessMode.createFromXml(in); |
| } |
| } |
| |
| if (blobHandle == null) { |
| Slog.wtf(TAG, "blobHandle should be available"); |
| return null; |
| } |
| if (blobAccessMode == null) { |
| Slog.wtf(TAG, "blobAccessMode should be available"); |
| return null; |
| } |
| |
| final BlobStoreSession blobStoreSession = new BlobStoreSession(context, sessionId, |
| blobHandle, ownerUid, ownerPackageName, stateChangeListener); |
| blobStoreSession.mBlobAccessMode.allow(blobAccessMode); |
| return blobStoreSession; |
| } |
| } |