React Native 打包簽名方案
@Allen fengjun.dev@gmail.com
我們都知道,RN開發(fā)完成后,不管是集成到app中一起發(fā)布還是熱更新發(fā)布,都會(huì)涉及到j(luò)s代碼的打包。如果代碼中引用了靜態(tài)圖片資源,還需要連同圖片一起打包。除此以外,還要想辦法保證我們所執(zhí)行的js代碼的安全性和完整性,基于這些需求,我們形成了一套R(shí)N打包簽名的方案,本文主要對(duì)這套方案記錄,以供參考。
靜態(tài)圖片加載方式的選擇
根據(jù)官方文檔,加載靜態(tài)圖片資源一共有兩種方式。第一種是將圖片放置在原生app的res/drawable目錄中,然后在js代碼中使用下面方式加載:
<Image source={{uri: 'app_icon'}} style={{width: 40, height: 40}} />
但是這樣的方式并不優(yōu)雅,不僅會(huì)增大宿主APP的體積,還會(huì)導(dǎo)致js層的實(shí)現(xiàn)與原生層耦合在一起,當(dāng)熱更新時(shí)無法做到圖片的更新。
第二種方式是采用相對(duì)路徑的方式進(jìn)行加載,例如,我們有以下的目錄結(jié)構(gòu)
src
├── button.js
└── img
├── check.png
└── check_pressed.png
然后在button.js這樣引用:
<Image source={require('./img/check.png')} />
這樣的方式就比較靈活,只要將js代碼和使用到的圖片打包在一起,就可以在熱更新的時(shí)候做到業(yè)務(wù)代碼和圖片的同步更新。
所以我們在項(xiàng)目中統(tǒng)一采用的第二種方案去加載靜態(tài)圖片。
對(duì)圖片路徑的定制
我們希望打包時(shí),整個(gè)RN包內(nèi)部是下面的結(jié)構(gòu),將所有圖片放置在images目錄下,這樣的目錄結(jié)構(gòu)比較清晰明了
├── images
│ ├── drawable-hdpi
│ ├── drawable-mdpi
│ ├── drawable-xhdpi
│ ├── drawable-xxhdpi
│ └── drawable-xxxhdpi
└── index.android.bundle
但是這里有個(gè)問題,release版本中,RN采用require方式加載圖片時(shí),生成的路徑是下面的形式:
<bundle當(dāng)前路徑>/drawable-mdpi/foldername_imagename.png
根據(jù)上面打包的結(jié)構(gòu),需要定制成為以下的形式才能正確訪問到圖片:
<bundle當(dāng)前路徑>/images/drawable-mdpi/foldername_imagename.png
所以就涉及到了對(duì)圖片路徑定制的需求,經(jīng)過RN源碼的探索,發(fā)現(xiàn)對(duì)于圖片路徑的解析是由node_modules/react-native/Libraries/Image/resolveAssetSource.js來完成的,解析生成圖片路徑主要代碼如下:
/**
* `source` is either a number (opaque type returned by require('./foo.png'))
* or an `ImageSource` like { uri: '<http location || file path>' }
*/
function resolveAssetSource(source: any): ?ResolvedAssetSource {
if (typeof source === 'object') {
return source;
}
var asset = AssetRegistry.getAssetByID(source);
if (!asset) {
return null;
}
const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
}
function setCustomSourceTransformer(
transformer: (resolver: AssetSourceResolver) => ResolvedAssetSource,
): void {
_customSourceTransformer = transformer;
}
可以看到,我們可以通過調(diào)用setCustomSourceTransformer來設(shè)置自己的AssetSourceResolver來完成自定義的路徑解析生成器,加上需要的images這一部分路徑即可。
Bundle打包和圖片導(dǎo)出
主要是使用官方提供的react-native bundle命令實(shí)現(xiàn)這個(gè)過程,如
react-native bundle --entry-file index.android.js --bundle-output ./output/my.bundle --dev false --platform android --assets-dest ./output/images/
具體的參數(shù)可以通過--help查看說明,需要注意的是,assets-dest指定的就是js代碼中引用到的圖片資源的導(dǎo)出路徑,如果不指定,將不會(huì)導(dǎo)出圖片。
命令執(zhí)行完成后,output目錄下:
├── images
│ ├── drawable-hdpi
│ ├── drawable-mdpi
│ ├── drawable-xhdpi
│ ├── drawable-xxhdpi
│ └── drawable-xxxhdpi
└── index.android.bundle
簽名與打包
在完成了上面的操作后,我們只是得到了一個(gè)文件夾,里面包含了bundle文件和圖片文件,如果涉及到中間人攻擊,bundle文件可能會(huì)被篡改,安全無法得到保證,所以我們需要對(duì)這個(gè)文件夾下的文件進(jìn)行加簽處理。具體采用的是下面的簽名方案:

(1)生成MANIFEST.MF文件:這是摘要文件。遍歷build目錄所有文件(entry),對(duì)非文件夾、非簽名文件的文件,逐個(gè)使用SHA1生成摘要信息,再用Base64編碼。并在摘要文件頭部寫入bundle的版本信息等。
說明:如果有人改變了安裝包中的文件,那么在安裝校驗(yàn)的時(shí)候,改變后的文件摘要信息與MANIFEST.MF的校驗(yàn)信息不同,將無法安裝。但是,如果攻擊者重新生成了摘要文件,就可以通過驗(yàn)證,所以這只是一個(gè)非常簡單的驗(yàn)證。需要結(jié)合(2)確保安全性。
(2)生成CERT.SF文件:這是摘要文件的簽名文件。對(duì)前一步生成的MANIFEST.MF,使用SHA1-RSA算法,用私鑰進(jìn)行簽名。在安裝時(shí),在客戶端使用公鑰進(jìn)行解密,解密之后MANIFEST.MF的內(nèi)容進(jìn)行比對(duì),如果相符,則表明內(nèi)容沒有被異常修改。
說明:在這一步,即使攻擊者修改了內(nèi)容,并生成了新的摘要文件,但是攻擊者沒有開發(fā)者的私鑰,所以不能生成正確的簽名文件(CERT.SF)??蛻舳嗽趯?duì)安裝包進(jìn)行驗(yàn)證的時(shí)候,用公鑰對(duì)不正確的簽名文件進(jìn)行解密,得到的結(jié)果和摘要文件(MANIFEST.MF)對(duì)應(yīng)不起來,所以不能通過檢驗(yàn),不能成功安裝文件。從而確保了安全性。
完成上述流程后,將簽名文件和bundle等文件一起壓縮,就完成了整個(gè)打包的流程。