Skip to content

Instantly share code, notes, and snippets.

@plugnburn
Last active October 11, 2023 12:49
Show Gist options
  • Save plugnburn/00fa61006513cdb0a12adf61a6e425e1 to your computer and use it in GitHub Desktop.
Save plugnburn/00fa61006513cdb0a12adf61a6e425e1 to your computer and use it in GitHub Desktop.
LibWallace: toolbox library for Qualcomm-based phones running KaiOS
/**
* LibWallace: toolbox library for Qualcomm-based and MTK-based phones running KaiOS
*
* Full support: KaiOS 2.5+ Nokias (Nokia 8110 4G, Nokia 2720 Flip, Nokia 800 Tough)
* Partial support: CAT B35, KaiOS 1.0 devices (Alcatel OT-4044O), MTK devices (Sigma S3500 sKai)
*
* Needs "certified" level in the app manifest.
* Requires additional manifest permissions:
*
* "power" - enable power management and privileged factory reset;
* "jrdextension" - support oldest devices in legacy mode;
* "kaiosextension" - support KaiOS 2.5 devices with unprotected extension;
* "engmode-extension" - support KaiOS 2.5.1+ devices with protected extension;
* "external-api" - support KaiOS 2.5.1+ devices with non-standard protected extension (addition to "engmode-extension");
* "device-storage:apps":{ "access": "readwrite" } - support package installation;
* "webapps-manage" - support package installation (via navigator.mozApps interface);
* "settings":{ "access": "readwrite" } - support hidden settings manipulation.
*
* Library functions marked as [EXPERIMENTAL] may work or fail to work on a particular device
* and/or with particular parameters.
* Library functions marked as [DANGEROUS] may cause system damage on a particular device.
*
* Current version: 0.6
*
* Version history:
*
* 2020-07-05: Added two new experimental MediaTek-specific methods: setMtkBluetoothMAC and setMtkWlanMAC
* 2020-06-18: Added the first MediaTek-specific method: setMtkIMEI
* 2020-04-21: Added generateRandomMAC, setNokiaBluetoothMAC and setNokiaWlanMAC methods
* 2019-12-07: Added enableCallRecording method
* 2019-11-30: Updated generateRandomIMEI method to optionally use a particular TAC
* for generating the IMEIs of a particular device model
* 2019-10-08: Initial release (v0.1)
*
* @license
* -----------------------------------------------------------------------
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* For more information, please refer to <http://unlicense.org/>
**/
;(function(global, nav) {
var masterExt = nav.engmodeExtension || nav.jrdExtension || nav.kaiosExtension
/**
* Run a system command as root (wrapper for extension methods)
* @param {string} cmd Command to run
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function runCmd(cmd, successCb, errorCb) {
if(!successCb) successCb = function(){}
if(!errorCb) errorCb = function(){}
var executor = masterExt.startUniversalCommand(cmd, true)
executor.onsuccess = successCb
executor.onerror = errorCb
}
/**
* Extract a file from application archive to the specified system path
* Requires Busybox on the system
* @param {string} appId Application ID (origin) to extract from
* @param {string} sourcePath The relative path to the file within the application
* @param {string} destPath The absolute path in the FS (with the filename) to extract to
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function extractAppAsset(appId, sourcePath, destPath, successCb, errorCb) {
var zipPath = '/data/local/webapps/' + appId + '/application.zip',
unzipCmd = 'busybox unzip -p ' + zipPath + ' ' + sourcePath + ' > ' + destPath
runCmd(unzipCmd, successCb, errorCb)
}
/**
* Start the privileged factory reset procedure
*/
function privilegedFactoryReset() {
nav.mozPower.factoryReset('root')
}
/**
* Get a property from the system Android property set
* (equivalent to getprop command)
* @param {string} key The name of the property to retrieve
*/
function getSystemProperty(key) {
return masterExt.getPropertyValue(key)
}
/**
* Set a property in the system Android property set
* (equivalent to setprop command)
* @param {string} key The name of the property to set
* @param {string} value The value of the property to set
*/
function setSystemProperty(key, value) {
masterExt.setPropertyValue(key, value)
}
/**
* Get a property from the system preference configuration
* @param {string} key The name of the preference to retrieve
* @param {string} defVal Optional default value to use if the preference is not set
*/
function getSystemPreference(key, defVal = null) {
return masterExt.getPrefValue(key, defVal)
}
/**
* [EXPERIMENTAL] Set a property in the system preference configuration
* @param {string} key The name of the preference to set
* @param {string} value The value of the preference to set
*/
function setSystemPreference(key, value) {
masterExt.setPrefValue(key, value)
}
/**
* Get a property from the system settings database
* @param {string} key The name of the property to retrieve
* @param {function} successCb The callback to which the value gets returned
* @param {function} errorCb The callback that gets called on error
*/
function getSystemSetting(key, successCb, errorCb) {
var e = nav.mozSettings.createLock().get(key)
e.onsuccess = function() {
successCb(e.result[key])
}
e.onerror = errorCb
}
/**
* Set a property in the system settings database
* @param {string} key The name of the property to set
* @param {string} value The value of the property to set
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setSystemSetting(key, value, successCb, errorCb) {
var setting = {}
setting[key] = value
var e = nav.mozSettings.createLock().set(setting)
e.onsuccess = successCb
e.onerror = errorCb
}
/*
* Toggle Qualcomm Diagnostics port in USB configuration until reboot
* (bypassing all launcher-side code protection)
* @param {function} successOnCb The callback that gets called on successful enabling
* @param {function} successOffCb The callback that gets called on successful disabling
* @param {function} errorCb The callback that gets called on error
*/
function toggleDiagPort(successOnCb, successOffCb, errorCb) {
var status = getSystemProperty('sys.usb.config'),
sysProp = 'persist.sys.usb.config',
inactiveState = 'mtp,adb',
activeState = 'diag,serial_smd,rmnet_qti_bam,adb'
if(status === activeState) { //diag enabled, disabling
setSystemProperty(sysProp, inactiveState)
successOffCb()
}
else { //diag disabled, enabling
setSystemProperty(sysProp, activeState)
setSystemSetting('ums.enabled', false, successOnCb, errorCb) //diag is incompatible with mass storage, so disabling it
}
}
/**
* [EXPERIMENTAL] Set system-wide proxy configuration (requires reboot or b2g process restart)
* @param {object} config Configuration object
*
* Implementation is still incomplete and relies upon Mozilla docs, not real KaiOS environment state.
* For the time being, it's recommended to use setBrowserProxy/unsetBrowserProxy methods instead whenever possible.
*
* Available fields:
*
* - type: 0 - direct (no proxy), 1 - manual, 2 - PAC
* - http: hostname or IP of plain HTTP proxy
* - http_port: 1-65535, 0 to ignore for HTTP URLs
* - ftp: hostname or IP of FTP proxy
* - ftp_port: 1-65535, 0 to ignore for FTP URLs
* - ssl: hostname or IP of HTTPS proxy
* - ssl_port: 1-65535, 0 to ignore for HTTPS URLs
* - socks: hostname or IP of SOCKS proxy
* - socks_port: 1-65535, 0 to ignore
* - socks_remote_dns: if using SOCKS 5, this flag controls whether lookups are done locally (default) or on the SOCKS server
* - socks_version: 4 or 5, default is 5
* - no_proxies_on: comma-separated list of IPs or hostnames to ignore proxying
* - autoconfig_url: absolute PAC url if set
* - failover_timeout: timeout in minutes for primary proxy connection
*
* See this article for details: https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Mozilla_networking_preferences
*/
function setSystemProxyConfig(config) {
var allowedPrefs = ['type', 'http', 'http_port', 'ftp', 'ftp_port', 'ssl', 'ssl_port', 'socks',
'socks_port', 'socks_remote_dns', 'socks_version', 'no_proxies_on', 'autoconfig_url', 'failover_timeout'],
pref
for(pref in config) {
if(allowedPrefs.indexOf(pref) > -1)
setSystemPreference('network.proxy.' + pref, config[pref])
}
}
/**
* Set browser-wide HTTP/HTTPS proxy configuration
* @param {string} host Proxy hostname or IP
* @param {number} port Proxy port number
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setBrowserProxy(host, port, successCb, errorCb) {
setSystemSetting('browser.proxy.host', host, function() {
setSystemSetting('browser.proxy.port', port, function() {
setSystemSetting('browser.proxy.enabled', true, successCb, errorCb)
}, errorCb)
}, errorCb)
}
/**
* Disable browser-wide HTTP/HTTPS proxy
*/
function unsetBrowserProxy(successCb, errorCb) {
setSystemSetting('browser.proxy.enabled', false, successCb, errorCb)
}
/**
* [EXPERIMENTAL] Set browser engine user agent
* Cautions:
* 1) affects stock KaiStore access
* 2) there's no way to reset it other than through WebIDE, factory reset or setting a new one
* @param {string} newUA The new user agent to set
*/
function setUserAgent(newUA) {
setSystemPreference('general.useragent.override', newUA)
}
/**
* Calculate Luhn checksum of an IMEI number string (takes first 14 digits and computes the 15th)
* @param {string|array} IMEI digit string or numeric array
* @returns {number} Luhn checksum digit
*/
function calcIMEIChecksum(imei) {
if(imei === '' + imei) //split the digits if we passed the string
imei = imei.split('').map(Number)
var revmap = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9],
oddsum = imei[0] + imei[2] + imei[4] + imei[6] + imei[8] + imei[10] + imei[12],
evensum = revmap[imei[1]] + revmap[imei[3]] + revmap[imei[5]] + revmap[imei[7]] + revmap[imei[9]] + revmap[imei[11]] + revmap[imei[13]],
luhn = 10 - (oddsum + evensum) % 10
return luhn > 9 ? 0 : luhn
}
/**
* Generate a random 15-digit IMEI that passes Luhn checksum
* @param {string} tac Optional TAC variable length (typically 8-digit) string
* @returns {string} random valid IMEI number
*/
function generateRandomIMEI(tac='') {
tac += ''
var imei = new Uint8Array(14 - tac.length).map(x=>(Math.random()*1000|0)%10)
imei = tac.split('').map(Number).concat(Array.from(imei))
return imei.join('') + calcIMEIChecksum(imei)
}
/**
* Generate a random 6-byte MAC address string
* @param {string} pref Optional hex vendor prefix string (can be colon-separated)
* @returns {string} random colon-separated MAC address
*/
function generateRandomMAC(pref='') {
pref = pref.toLowerCase().replace(/[^0-9a-f]/g, '')
var mac = new Uint8Array(12 - pref.length).map(x=>(Math.random()*1000|0)&15)
mac = pref.split('').map(x => parseInt(x, 16)).concat(Array.from(mac))
return mac.map(x => x.toString(16)).join('').match(/.{2}/g).join(':')
}
/**
* [EXPERIMENTAL] [DANGEROUS] IMEI editor - Nokias only!
* Requires reboot. Any error will lead to radio failure until EFS is restored.
* @param {number} sim SIM number (1 or 2)
* @param {string} imei New IMEI 15-digit string
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setNokiaIMEI(sim, imei, successCb, errorCb) {
if(Number(imei[14]) !== calcIMEIChecksum(imei)) {
errorCb() //call the error callback if IMEI has invalid checksum
return
}
var targetFile = sim === 2 ? 'nvm/context1/550' : 'nvm/num/550',
qPayload = '\\x' + ('80a' + imei).match(/.{2}/g).map(function(c){return c[1] + c[0]}).join('\\x'),
blkPref = '/dev/block/bootdevice/by-name',
tunPart = blkPref + '/tunning',
efs1 = blkPref + '/modemst1',
efs2 = blkPref + '/modemst2',
cmdbatch = [
'cd $(mktemp -d)',
'busybox tar xf ' + tunPart,
'echo -ne "' + qPayload + '" > ' + targetFile,
'busybox tar cf - . > ' + tunPart,
'dd if=/dev/zero of=' + efs1 + '; dd if=/dev/zero of=' + efs2
].join(' && ')
runCmd(cmdbatch, successCb, errorCb)
}
/**
* [EXPERIMENTAL] [DANGEROUS] WLAN MAC address editor - Nokias only!
* Requires reboot. Any error will lead to radio failure until EFS is restored.
* @param {string} mac New MAC 6-byte string (can be just hex or colon separated)
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setNokiaWlanMAC(mac, successCb, errorCb) {
var targetFile = 'nvm/num/4678',
qPayload = '\\x' + mac.toLowerCase().replace(/[^0-9a-f]/g, '').match(/.{2}/g).join('\\x'),
blkPref = '/dev/block/bootdevice/by-name',
tunPart = blkPref + '/tunning',
efs1 = blkPref + '/modemst1',
efs2 = blkPref + '/modemst2',
cmdbatch = [
'cd $(mktemp -d)',
'busybox tar xf ' + tunPart,
'echo -ne "' + qPayload + '" > ' + targetFile,
'busybox tar cf - . > ' + tunPart,
'dd if=/dev/zero of=' + efs1 + '; dd if=/dev/zero of=' + efs2
].join(' && ')
runCmd(cmdbatch, successCb, errorCb)
}
/**
* [EXPERIMENTAL] [DANGEROUS] Bluetooth MAC address editor - Nokias only!
* Requires reboot. Any error will lead to radio failure until EFS is restored.
* @param {string} mac New MAC 6-byte string (can be just hex or colon separated)
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setNokiaBluetoothMAC(mac, successCb, errorCb) {
var targetFile = 'nvm/num/447',
persistFile = '/persist/bluetooth/.bt_nv.bin',
qPayload = '\\x' + mac.toLowerCase().replace(/[^0-9a-f]/g, '').match(/.{2}/g).join('\\x'),
pPayload = '\\x01\\x01\\x06' + qPayload,
blkPref = '/dev/block/bootdevice/by-name',
tunPart = blkPref + '/tunning',
efs1 = blkPref + '/modemst1',
efs2 = blkPref + '/modemst2',
cmdbatch = [
'cd $(mktemp -d)',
'busybox tar xf ' + tunPart,
'echo -ne "' + qPayload + '" > ' + targetFile,
'busybox tar cf - . > ' + tunPart,
'echo -ne "' + pPayload + '" > ' + persistFile,
'dd if=/dev/zero of=' + efs1 + '; dd if=/dev/zero of=' + efs2
].join(' && ')
runCmd(cmdbatch, successCb, errorCb)
}
/**
* [EXPERIMENTAL] [DANGEROUS] IMEI editor - MediaTeks only!
* Requires reboot. Any error will lead to radio failure until NVRAM is restored.
* @param {number} sim SIM number (1 or 2)
* @param {string} imei New IMEI 15-digit string
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setMtkIMEI(sim, imei, successCb, errorCb) {
if(Number(imei[14]) !== calcIMEIChecksum(imei)) {
errorCb() //call the error callback if IMEI has invalid checksum
return
}
var targetEgmrIndex = sim === 2 ? 10 : 7,
egmrCommand = 'echo -e \'AT\\r\\nAT+EGMR=1,' + targetEgmrIndex + ',"' + imei + '"\\r\\n\' >> /dev/radio/pttycmd1'
runCmd(egmrCommand, successCb, errorCb)
}
/**
* [EXPERIMENTAL] [DANGEROUS] WLAN MAC address editor - MediaTeks only!
* Requires WLAN restart. Any error will lead to radio failure until NVRAM is restored.
* On some devices, this function may only work until the reboot.
* @param {string} mac New MAC 6-byte string (can be just hex or colon separated)
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setMtkWlanMAC(mac, successCb, errorCb) {
var targetFile = '/data/nvram/APCFG/APRDEB/WIFI', tmpFile='/cache/mac',
startOffset=4,
dPayload = '\\x' + mac.toLowerCase().replace(/[^0-9a-f]/g, '').match(/.{2}/g).join('\\x'),
cmdbatch = [
'echo -ne "' + dPayload + '" > ' + tmpFile,
'dd if=' + tmpFile + ' of=' + targetFile + ' bs=1 seek=' + startOffset + ' conv=notrunc',
'rm ' + tmpFile
].join(';')
runCmd(cmdbatch, successCb, errorCb)
}
/**
* [EXPERIMENTAL] [DANGEROUS] Bluetooth MAC address editor - MediaTeks only!
* Requires Bluetooth restart. Any error will lead to radio failure until NVRAM is restored.
* On some devices, this function may only work until the reboot.
* @param {string} mac New MAC 6-byte string (can be just hex or colon separated)
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function setMtkBluetoothMAC(mac, successCb, errorCb) {
var targetFile = '/data/nvram/APCFG/APRDEB/BT_Addr', tmpFile='/cache/mac',
dPayload = '\\x' + mac.toLowerCase().replace(/[^0-9a-f]/g, '').match(/.{2}/g).join('\\x'),
cmdbatch = [
'echo -ne "' + dPayload + '" > ' + tmpFile,
'dd if=' + tmpFile + ' of=' + targetFile + ' bs=1 conv=notrunc',
'rm ' + tmpFile
].join(';')
runCmd(cmdbatch, successCb, errorCb)
}
/**
* Fix outgoing packet TTL for tethering purposes (until reboot)
* Currently there is Qualcomm support only!
* @param {number} ttlval New fixed TTL value (64 recommended)
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function fixTTL(ttlval, successCb, errorCb, iface) {
if(!iface) iface = 'rmnet_data0'
var ttlprev = ttlval - 1,
cmdbatch = [
'iptables -t filter -N sort_out_interface',
'iptables -t filter -A sort_out_interface -m ttl --ttl-lt ' + ttlprev + ' -j REJECT',
'iptables -t filter -A sort_out_interface -m ttl --ttl-eq ' + ttlprev + ' -j RETURN',
'iptables -t filter -A sort_out_interface -j CONNMARK --set-mark ' + ttlval,
'iptables -t filter -I FORWARD -o ' + iface + ' -j sort_out_interface',
'iptables -t filter -I OUTPUT -o ' + iface + ' -j sort_out_interface',
'ip rule add fwmark ' + ttlval + ' table 164',
'ip route add default dev lo table 164',
'ip route flush cache'
].join(' && ')
runCmd(cmdbatch, successCb, errorCb)
}
/**
* Install an app package via the stock B2G API
* @param {File|Blob} packageFile The File or Blob object of the zip package to install
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function installPkg(packageFile, successCb, errorCb) {
nav.mozApps.mgmt.import(packageFile).then(function(){
successCb()
}).catch(function(e){
errorCb(e.name, e.message)
})
}
/**
* Reboot the phone
* @param {function} errorCb The callback that gets called on error
*/
function reboot(errorCb) {
runCmd('reboot', function(){}, errorCb)
}
/**
* Enable/disable call recording in the main callscreen (KaiOS 2.5.2+ only)
* (toggled by Camera button on the devices that have it and Left arrow on all others)
* @param {string} flag (on|auto|off)
* @param {string} format Recording format (wav, 3gpp, ogg or opus)
* @param {function} successCb The callback that gets called on success
* @param {function} errorCb The callback that gets called on error
*/
function enableCallRecording(flag='on', format='wav', successCb, errorCb) {
setSystemSetting('callrecording.mode', flag, function() {
setSystemSetting('callrecording.file.format', format, function() {
setSystemSetting('callrecording.notification.enabled', false, function() {
setSystemSetting('callrecording.vibration.enabled', false, successCb, errorCb)
}, errorCb)
}, errorCb)
}, errorCb)
}
global.Wallace = {
runCmd: runCmd,
extractAppAsset: extractAppAsset,
privilegedFactoryReset: privilegedFactoryReset,
getSystemProperty: getSystemProperty,
setSystemProperty: setSystemProperty,
getSystemPreference: getSystemPreference,
setSystemPreference: setSystemPreference,
getSystemSetting: getSystemSetting,
setSystemSetting: setSystemSetting,
toggleDiagPort: toggleDiagPort,
setSystemProxyConfig: setSystemProxyConfig,
setBrowserProxy: setBrowserProxy,
unsetBrowserProxy: unsetBrowserProxy,
setUserAgent: setUserAgent,
calcIMEIChecksum: calcIMEIChecksum,
generateRandomIMEI: generateRandomIMEI,
generateRandomMAC: generateRandomMAC,
setNokiaIMEI: setNokiaIMEI,
setNokiaWlanMAC: setNokiaWlanMAC,
setNokiaBluetoothMAC: setNokiaBluetoothMAC,
setMtkIMEI: setMtkIMEI,
setMtkWlanMAC: setMtkWlanMAC,
setMtkBluetoothMAC: setMtkBluetoothMAC,
fixTTL: fixTTL,
installPkg: installPkg,
reboot: reboot,
enableCallRecording: enableCallRecording
}
})(window, navigator)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment