关于 Android 的 OMA DRM 验证

记录这么一个有些过时了的 DRM 方案, 不是初衷,只是之前测试部门抛过来一个问题: “我们需不需要像 xx 厂商一样去搭建一系列 OMA DRM 用例? ”
本来按计划,应该早些投身于 Widevine 的规划的,但忽然公司关于整个项目的时间表 delay 了, 呵呵, 忽然发觉还是局外人看的准, 好吧,闲着也是闲着, 评估一下 OMA DRM 吧;  // MAGIC1.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu

关于 Android 的 OMA DRM 验证_第1张图片

为偷懒, OMA DRM 的 SPEC 这里不做描述了,  OMA 是 Open Mobile Alliance, 可以去 http://openmobilealliance.org/wp/index.html 下载所有 spec , 如 http://openmobilealliance.org/release/DRM/
Android 的 DRM Framework 这里不做描述了, 可以百度出一堆, 这一块框架也多年未变;

这里只描述下 Android 环境下对 OMA DRM 的验证, 准确的说是对 Forward Lock 的验证;
只验 FL 一来是因为 Android 只有 FL 的 plugin : frameworks/av/drm/libdrmframework/plugins/forward-lock/FwdLockEngine
某通倒是提供了更全的插件库, 可以支持 FL/CD/SD 且同时包含了更多的文件类型, 给出了 native 实现和 java demo, 但是某通在它的 oma drm doc 中鄙视了一番 android 之后, 随后在 Android M 之后的版本就把它炫耀的这些插件库从自己的编译脚本中移除,哎, 不知道怎么形容这货 ~   // MAGIC2  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu
二来是因为 Combined Delivery 和 Seperate Delivery 搭建服务器会麻烦一些,  除 http server 外还需要建一个 Push Initiator 来传 Rights 文件 ;
如下图:

关于 Android 的 OMA DRM 验证_第2张图片

先从代码走起吧, 我们能从 oma drm spec 图上看到 Forward-Lock 传送的是明文,  服务器上的文件发送到设备端, 并没有做加密, 除了添加 dm 信息;
那是设备端下载文件的时候做加密吗?  是的...   下载的时候调用 android DRM api 的 convertData 来加密 !
上层封装是在 frameworks/base/drm/java/android/drm/DrmOutputStream.java 

    public void write(byte[] buffer, int offset, int count) throws IOException {
        Arrays.checkOffsetAndCount(buffer.length, offset, count);

        final byte[] exactBuffer;
        if (count == buffer.length) {
            exactBuffer = buffer;
        } else {
            exactBuffer = new byte[count];
            System.arraycopy(buffer, offset, exactBuffer, 0, count);
        }

        final DrmConvertedStatus status = mClient.convertData(mSessionId, exactBuffer);
        if (status.statusCode == STATUS_OK) {
            IoBridge.write(mFd, status.convertedData, 0, status.convertedData.length);
        } else {
            throw new IOException("Unexpected DRM status: " + status.statusCode);
        }
    }

这个 DrmOutputStream 就是在下载时候使用的,   就是你们看到的,在 packages/providers/DownloadProvider/   -----

    private void transferData(HttpURLConnection conn) throws StopRequestException {

        DrmManagerClient drmClient = null;
        ParcelFileDescriptor outPfd = null;
        FileDescriptor outFd = null;
        InputStream in = null;
        OutputStream out = null;
        try {
            try {
                in = conn.getInputStream();
            } catch (IOException e) {
                throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
            }

            try {
                outPfd = mContext.getContentResolver()
                        .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
                outFd = outPfd.getFileDescriptor();

                if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
                    drmClient = new DrmManagerClient(mContext);
                    out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);

这里 isDrmConvertNeeded 的条件是比对 mimetype 是否为 "application/vnd.oma.drm.message" ;
就是说, 如果使用 Android 的 DownloadProvider 去下载服务器上 oma.drm 类型 mimetype 的文件, 那么就会启动 DrmOutputStream  来加密下载后的文件;  本来服务器上存的是原始文件如 .mp4, 加上 drm message 信息后, 后缀名一般改为 .dm,  设备端 DownloadProvider 下载和加密之后就转为 .fl 后缀;  // MAGIC3.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu

大家这里应该猜到了, 这加密看起来和 Android DownloadProvider 绑的太紧, 如果换个浏览器, 有自己的 DownloadProvider 是什么情况?   没错, 实践证明, 换成 360 Browser 或者 UC Browser , 这一套加密就没戏了 ...  

关于 Android 的 OMA DRM 验证_第3张图片


好吧,继续, 加密完了哪里解密呢?   在 Extractor 里...     oma drm 是所谓 container_based 

void DataSource::RegisterDefaultSniffers() {
    Mutex::Autolock autoLock(gSnifferMutex);
    if (gSniffersRegistered) {
        return;
    }
    RegisterSniffer_l(SniffMPEG4);
    RegisterSniffer_l(SniffMatroska);
    RegisterSniffer_l(SniffOgg);
    RegisterSniffer_l(SniffWAV);
    RegisterSniffer_l(SniffFLAC);
    RegisterSniffer_l(SniffAMR);
    RegisterSniffer_l(SniffMPEG2TS);
    RegisterSniffer_l(SniffMP3);
    RegisterSniffer_l(SniffAAC);
    RegisterSniffer_l(SniffMPEG2PS);
    RegisterSniffer_l(SniffWVM);
    RegisterSniffer_l(SniffMidi);

    char value[PROPERTY_VALUE_MAX];
    if (property_get("drm.service.enabled", value, NULL)
            && (!strcmp(value, "1") || !strcasecmp(value, "true"))) {
        RegisterSniffer_l(SniffDRM);
    }
    gSniffersRegistered = true;
}

这里的 SniffDRM 就是去跑 FileSource::DrmInitialization, 这里会走到 DrmManager 的 openDecryptSession, 接下来就是挨个调用所有 plugin 的 onOpenDecryptSession 来检查是否类型匹配;
对 Forward-Lock plugin 来说, 就是检查当前这个 fl 文件的字段, 比如文件头同步字应该是 'F', "W', 'L', 'K' ...  打头的所谓 topHeader,  随后会解出 EncryptedKey, 然后还会验证 HMAC 解析出的 signature  和 fl 文件头读出的 signature 是否一致, 这些过程都 Pass 了才会返回 sniff 结果, 告知 mediaserver 这是个 oma drm 文件,  但是因为 FL plugin 是 container_based , 所以 extractor 不走 DRMExtractor,   而是在 FileSource 中直接 readAtDRM, 走 pread 接口来解密;

Forward-Lock 的中文译成转发锁定,  就是说本机加密的可以解密, 如果拷贝到别的机器上就解不了 ...   这个是怎么做到的?  从 Android 的 Forward-Lock plugin 代码可以看到加解密的流程 :
1)  对于加密, 首先是 generate 一个 16字节随机数, 作为 Global 的 EncryptionRoundKey,    这个 EncryptionRoundKey  被保存在 /data/drm/fwdlock/kek.dat ;
随后在 OpenSession 时再 generate 一个 16字节随机数作为 SessionKey ;  
用 global 的 EncryptionRoundKey , 和一个 16字节随机数 InitVector, 来 AES_encrypt 加密 SessionKey,    产生  EncryptSessionKey ;  // MAGIC4.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu
这个 EncryptSessionKey  会被写入生成的 fl 文件头;
最后, 用 SessionKey 去 AES_encrypt 16字节的 {0,...0} 产生 Session 的 EncryptionRoundKey,  这个 Session 的 EncryptionRoundKey 就是最终用来给文件数据做加密的 key ,  此过程体现在  FwdLockConv_WriteEncryptedChar 中;   最终写入 fl 文件的, 就是加密后的数据, 前面再带上 EncryptSessionKey, 加密数据的 DataSignature, 和对应 topHeader, EncryptSessionKey, DataSignature 的  HeaderSignature;


2) 解密的时候, 先从 fl 文件头读入 EncryptSessionKey, 读入 DataSignature, HeaderSignature,  随后用 /data/drm/fwdlock/kek.dat 保存的 Global  EncryptionRoundKey  来 decrypt  那个  EncryptSessionKey, 生成 DecryptedSessionKey ; // MAGIC5.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu
然后用 DecryptedSessionKey 来 AES_encrypt 16字节的 {0,...0}  生成 Session 的 EncryptionRoundKey ;   这个 EncryptionRoundKey  就是用来给数据做解密的;
这中间还包含对 HeaderSignature 校验过程,   Forward-Lock  转发锁定就在于, 如果 fl 文件拷贝到了另一台样机, 那么新样机的  /data/drm/fwdlock/kek.dat, 作为一个16字节随机数, 是不可能和原样机相同的, 这就不可能生成和原样机相同的 SessionKey,   这样在校验 HeaderSignature 的时候就失败了,  就达到了锁定的目的;


代码分析完毕, 验证一下吧; 先是搭建 oma drm 服务器, 为了生成 drm message, 对于 FL 来说这个 message 较简单, 可以手动添加, 也可以使用工具, 比如可以下载到 Sony Ericsson DRM Packager , 如下图:
关于 Android 的 OMA DRM 验证_第4张图片 


用工具生成 .dm 文件后, 挂到 http 服务器上面, 如前所述, 记得修改 .dm 的 mimetype 为  "application/vnd.oma.drm.message", 如下图 // MAGIC6.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu

关于 Android 的 OMA DRM 验证_第5张图片


找一个 Android M 版本的样机, 再往后的版本  packages/app/Browser 就被移除了, 没有了原生浏览器就调不了原生的 DownloadProvider  , 就测不了;  使用浏览器下载 .dm 文件, 生成 .fl 文件后,  在本机上可以播放, 转发后无法播放;
贴一下原始文件, .dm 文件, .fl 文件的对比图: // MAGIC7.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu

左边是 mp4 原始文件, 中间是添加了 drm message 的 dm 文件,  右边是被加密后的 fl 文件;

关于 Android 的 OMA DRM 验证_第6张图片

 

最后的最后, 以上的所有一切其实都已是昨日黄花 ...    因为对一个 android 设备而言, 下载 oma drm 文件, Android M 版本之后由于移除了 Browser 就已经无法调用到  DownloadProvider, 导致无法加密;   同时 Android N 之后移除了 DRMExtractor 导致无法 sniff 出 oma drm 的 fl 文件;  可能是 google 收敛到 widevine 去了吧, 就像 Android P 上 google 移除了 miracast 只推 google cast ...  // MAGIC8.  DO NOT TOUCH http://blog.csdn.net/leonxu_sjtu

大江东去浪淘尽...  

关于 Android 的 OMA DRM 验证_第7张图片
 

你可能感兴趣的:(Android)