企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
在前文的分析中,我们略去了CBS中与权限管理相关的部分,本节将集中讨论这个问题。先来回顾一下CBS中和权限管理相关的函数调用。 ~~~ //copy方设置ClipData在CBS的setPrimaryClip函数中进行: checkDataOwnerLocked(clip,Binder.getCallingUid()); clearActiveOwnersLocked(); //paste方获取ClipData在CBS的getPrimaryClip函数中进行: addActiveOwnerLocked(Binder.getCallingUid(), pkg); ~~~ 在分析这3个函数之前,不妨先介绍一下Android系统中的URI权限管理。 1. URI权限管理介绍 Android系统的权限管理中有一类是专门针对URI的,先来看一个示例,该例来自package/providers/ContactsProvider,在它的AndroidManifest.xml中有如下声明: **AndroidManifest.xml** ~~~ <providerandroid:name="ContactsProvider2" ...... android:readPermission="android.permission.READ_CONTACTS" android:writePermission="android.permission.WRITE_CONTACTS"> ...... <grant-uri-permission android:pathPattern=".*" /> </provider> ~~~ 这里声明了一个名为“ContactsProvider2“的ContentProvider,并定义了几个权限声明,下面对其进行解释。 - readPermission:要求调用query函数的客户端必须声明use-permission为READ_CONTACTS。 - writePermission:要求调用update或insert函数的客户端必须声明use-permission为WRITE_CONTACTS。 - grant-uri-permission:和授权有关。 初识grant-uri-permission时,会觉得比较难以理解,现在通过举例分析帮助读者加深认识。 - Contacts和ContactProvider这两个APP都是由系统提供的程序,而且二者关系紧密,所以Contacts一定会声明READ_CONTACTS和WRITE_CONTACT的权限。如此,Contacts就可以毫无阻碍地通过ContactsProvider来查询或更新数据库了。 - 假设Contacts新增一个功能,将ContactsProvider中的某条数据copy到剪切板。根据前面已介绍过的知识可以知道,Contacts会向剪切板中复制一个URI类型的数据。 - 另外一个程序从剪切板中paste这条数据,由于是URI类型的,所以此程序会通过ContentResolver来query该URI所指向的数据。但是这个程序却并未声明READ_CONTACTS的权限,所以它query数据时必然会失败。 或许有人会问,为什么第三个程序不声明相应权限呢?原因很简单,第三个程序不知道自己该申明怎样的权限(除非这两个程序的开发者能互通信息)。本例ContactsProvider设置的读权限是READ_CONTACTS,以后可能换成READ_CONTACTS_EXTEND。为了解决类似问题,Android提供了一套专门针对URI的权限管理机制。以这套机制解决示例中权限声明问题的方法是这样的:当第三个程序从剪切板中paste数据时,系统会判断是否需要为这个程序授权。当然,系统不会随意授权,而是需要考虑ContactsProvider的情况。因为ContactsProvider声明了grant-uri-permission,并且所paste的URI匹配其中的pathPattern,所以授权就能成功。倘若ContactsProvider没有声明grant-uri-permission,或者URI不匹配指定的pathPattern,则授权失败。 有了前面介绍的权限管理机制,相信下面CBS中的权限管理理解起来就比较简单了。 感兴趣的读者可阅读SDK安装目录下/docs/guide/topics/security/security.html中关于URI Permission的说明部分。 2. checkDataOwnerLocked函数分析 checkDataOwnerLocked函数的代码如下: **ClipboardService.java** ~~~ private final void checkDataOwnerLocked(ClipDatadata, int uid) { //第二个参数uid为copy方进程的uid final int N = data.getItemCount(); for(int i=0; i<N; i++) { //为每一个item调用checkItemOwnerLocked checkItemOwnerLocked(data.getItemAt(i), uid); } } // checkItemOwnerLocked函数分析 private final voidcheckItemOwnerLocked(ClipData.Item item, int uid) { if(item.getUri() != null) {//检查Uri checkUriOwnerLocked(item.getUri(), uid); } Intent intent = item.getIntent(); //getData函数返回的也是一个URI,因此这里实际上检查的也是URI if(intent != null && intent.getData() != null) { checkUriOwnerLocked(intent.getData(), uid); } } ~~~ 权限检查就是针对URI的,因为URI所指向的数据可能是系统内部使用或私密的。例如Setting数据中的Secure表,这里的数据不能随意访问。虽然直接使用ContentResolver访问这些数据时系统会进行权限检查,但是由于目前的剪切板服务也支持URI数据类型,所以这里也需要做检查,否则恶意程序就能轻松读取私密信息。 下边来分析checkUriOwnerLocked函数,其代码如下: **ClipboardService.java** ~~~ private final void checkUriOwnerLocked(Uri uri,int uid) { ...... longident = Binder.clearCallingIdentity(); boolean allowed = false; try{ /* 调用ActivityManagerService的checkGrantUriPermission函数, 该函数内部将检查copy方是否能被赋予URI_READ权限。如果不允许, 该函数会抛SecurityException异常 */ mAm.checkGrantUriPermission(uid, null, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); }catch (RemoteException e) { }finally { Binder.restoreCallingIdentity(ident); } } ~~~ 根据前面的知识,这里先要检查copy方是否有读取URI的权限。下面来分析paste方的权限管理 3. clearActiveOwnersLocked函数分析 clearActiveOwnersLocked函数的代码如下: **ClipboardService.java** ~~~ private final void addActiveOwnerLocked(int uid,String pkg) { PackageInfo pi; try{ /* 调用PackageManagerService的getPackageInfo函数得到相关信息 然后做一次安全检查,如果PacakgeInfo的uid信息和当前调用的uid不一致, 则抛SecurityException。这个很好理解,因为paste方可以传递虚假的 packagename,但uid是没法造假的 */ pi = mPm.getPackageInfo(pkg, 0); if (pi.applicationInfo.uid != uid) { throw new SecurityException("Calling uid " + uid + " does not ownpackage " + pkg); } } ...... } //mActivePermissionOwners用来保存已经通过安全检查的package if(mPrimaryClip != null && !mActivePermissionOwners.contains(pkg)) { //针对ClipData中的每一个Item,都需要调用grantItemLocked来检查权限 final int N = mPrimaryClip.getItemCount(); for (int i=0; i<N; i++) { grantItemLocked(mPrimaryClip.getItemAt(i), pkg); }//保存package信息到mActivePermissionOwners mActivePermissionOwners.add(pkg); } } //grantItemLocked分析 private final void grantItemLocked(ClipData.Itemitem, String pkg) { if(item.getUri() != null) { grantUriLocked(item.getUri(), pkg); } //和copy方一样,这里仅检查URI的情况 Intent intent = item.getIntent(); if(intent != null && intent.getData() != null) { grantUriLocked(intent.getData(), pkg); } } ~~~ 再来看grantUriLocked的代码: **ClipboardService.java** ~~~ private final void grantUriLocked(Uri uri, Stringpkg) { longident = Binder.clearCallingIdentity(); try{ /* 调用ActivityManagerService的grantUriPermissionFromOwner函数, 注意第二个参数传递的是CBS所在进程的uid。该函数内部也会检查权限。 该函数调用成功后,paste方就被授予了对应URI的读权限 */ mAm.grantUriPermissionFromOwner(mPermissionOwner, Process.myUid(),pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); }catch (RemoteException e) { } finally { Binder.restoreCallingIdentity(ident); } } ~~~ 既然有授权,那么客户端使用完毕后就需要撤销授权,这个工作是在setPrimaryClip函数的clearActiveOwnersLocked中完成的。当为剪切板设置新的ClipData时,自然需要将与旧ClipData相关的权限撤销。读者可自行分析clearActiveOwnersLocked函数。