Analysis of cryptographic routines found within "Perenio Smart: Home and Office" Android application, as well as reconstructing Perenio Peifc01 IP camera's RTP over UDP video stream from a Wireshark packet capture
Python
0
5 commits
updated Sep 24, 2026
Analysis of cryptographic routines found within "Perenio Smart: Home and Office" Android application, as well as reconstructing Perenio Peifc01 IP camera's RTP over UDP video stream from a Wireshark packet capture
Using my custom Frida script for cryptographic method detection in Android applications, I captured Java Cipher class doing a decryption operation, along with it's stack trace:
{
"objectId": "Cipher-30236645",
"timestamp": 1789412643454,
"lastSeen": 1789412643488,
"instanceOverload": "1. [Cipher.getInstance(java.lang.String) -> static Cipher]",
"transformation": "AES/CBC/PKCS5Padding",
"algorithm": "AES/CBC/PKCS5Padding",
"runtimeClass": "javax.crypto.Cipher",
"providerName": "AndroidOpenSSL",
"providerVersion": 1,
"providerInfo": "Android's OpenSSL-backed security provider",
"providerClass": "com.android.org.conscrypt.OpenSSLProvider",
"updateInputs": [],
"updateInputsLen": [],
"updateOutputs": [],
"updateOutputsLen": [],
"initOverload": "6. [Cipher.init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) -> void]",
"opmode": 2,
"opmodeString": "DECRYPT",
"blockSize": 16,
"keyClass": "javax.crypto.spec.SecretKeySpec",
"keyAlgorithm": "AES",
"keyFormat": "RAW",
"keyBytesHex": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c",
"keyBytesString": "K.N.]{.........\\",
"keyBytesFingerprint": "32862b80:16",
"parameterClass": "javax.crypto.spec.IvParameterSpec",
"iv": "000000000000000000000000000000",
"ivFingerprint": "69691905:16",
"secureRandom": "java.security.SecureRandom",
"finalOverload": "2. [Cipher.doFinal(byte[] input) -> byte[]]",
"bytesWritten": 96,
"input": "7c354a11ebcff3abe4b3c1c359cb16c14a1685851abaca3fd80b2cb8ba1b423f398944c108b2ba6096d43fb0317c5c17 ... [64 more bytes]",
"output": "{\"commonPrefs\":{\"display_rate_us_after\":0,\"permi ... [48 more bytes]",
"outputFingerprint": "a29ad7a9:96",
"stackTrace": [
"java.lang.Throwable",
"at javax.crypto.Cipher.doFinal(Native Method)",
"at se.simbio.encryption.Encryption.decrypt(Encryption.java:179)",
"at se.simbio.encryption.Encryption.decryptOrNull(Encryption.java:193)",
"at com.perenio.data.util.EncryptionUtils.decrypt(EncryptionUtils.kt:21)",
"at com.perenio.data.local.sharedpreference.base.AbsEncryptPreferencesProvider.getStringDecrypt(AbsEncryptPreferencesProvider.kt:15)",
"at com.perenio.data.local.sharedpreference.base.AbsEncryptPreferencesProvider.getStringDecrypt$default(AbsEncryptPreferencesProvider.kt:13)",
"at com.perenio.data.local.sharedpreference.SharedPreferencesProvider.<init>(SharedPreferencesProvider.kt:149)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule.provideSharedPreferencesProvider(SharedPreferencesModule.kt:25)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.provideSharedPreferencesProvider(SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.java:56)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.get(SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.java:44)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.get(SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.java:14)",
"at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)",
"at com.perenio.smarthome.di.module.data.SectionsModule_ProvideUserSectionFactory.get(SectionsModule_ProvideUserSectionFactory.java:68)",
"at com.perenio.smarthome.di.module.data.SectionsModule_ProvideUserSectionFactory.get(SectionsModule_ProvideUserSectionFactory.java:19)",
"at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)",
"at com.perenio.smarthome.di.component.DaggerAppComponent$AppComponentImpl.synchronizeSubscriptionsUseCase(DaggerAppComponent.java:2416)",
"at com.perenio.smarthome.di.component.DaggerAppComponent$AppComponentImpl.injectPerenioApp(DaggerAppComponent.java:3815)",
"at com.perenio.smarthome.di.component.DaggerAppComponent$AppComponentImpl.inject(DaggerAppComponent.java:3036)",
"at com.perenio.smarthome.PerenioApp.onCreate(PerenioApp.kt:47)",
"at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1385)",
"at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7830)",
"at android.app.ActivityThread.handleBindApplication(Native Method)",
"at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)",
"at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2546)",
"at android.os.Handler.dispatchMessage(Handler.java:110)",
"at android.os.Looper.loopOnce(Looper.java:248)",
"at android.os.Looper.loop(Looper.java:338)",
"at android.app.ActivityThread.main(ActivityThread.java:9068)",
"at java.lang.reflect.Method.invoke(Native Method)",
"at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:596)",
"at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932)\n"
]
}
Name of the Android application is com.perenio.smarthome.
adb shell pm list packages | grep <package-name>
adb shell pm path <package-name>:package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/base.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_asset_pack.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_config.arm64_v8a.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_config.en.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_config.xxhdpi.apk
Core application logic is stored in base.apk with assets and configs stored in separate .apk files. For the purposes of current research, only the base application file was analyzed.
base.apk to host machine:adb pull /data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/base.apk
This section covers analysis of Perenio application's data encryption and decryption methods. Both static analysis with JADX and dynamic analysis using Frida were performed.
One important thing to note: the described encryption method is a local-only encryption method, i.e. it only protects data that is at rest in local storage. Data transmitted via network is protected by TLSv1.3 sessions and SSL certificates pinned into Perenio app.
Searching for se.simbio.encryption.Encryption in Jadx-gui leads to the decompiled code of the Encryption class (whose scope is set to public).
The class has getDefault(String str, String str2, byte[] bArr) { ... } method that creates an object of Builder class, responsible for providing information necessary to construct key and initialization vector (IV) for AES encryption:

This method is called from com.perenio.data.util.EncryptionUtils class and gets hardcoded key and salt values provided as arguments:

The builder method itself builds a key from provided information and has setters and getters for every private variable:

Overloading the getDefaultBuilder method (along with decrypt method) using Frida discloses that the following information is being received during Perenio application's initialization sequence:
During the app initialization stage, Frida overloads for getDefaultBuilder and decrypt methods captured the following information about building a key and IV for AES decryption operation:
{
"threadId": "2",
"builder": {
"algorithm": "AES/CBC/PKCS5Padding",
"modeBase64": 0,
"charsetName": "UTF8",
"digestAlgorithm": "SHA1",
"iterationCount": 1,
"iv": "00000000000000000000000000000000",
"ivParameterSpec": null,
"key": "A45BE7EF0543D36B4F68DvD3EF4013D4",
"keyAlgorithm": "AES",
"keyLength": 128,
"salt": "BDBA982FAF389694",
"secretKeyType": "PBKDF2WithHmacSHA1",
"secureRandom": null,
"secureRandomAlgorithm": "SHA1PRNG"
},
"getDefaultInfo": {
"key": "A45BE7EF0543D36B4F68DvD3EF4013D4",
"salt": "BDBA982FAF389694",
"defaultKey": "00000000000000000000000000000000"
},
"encrypted": "fDVKEevP86vks8HDWcsWwUoWhYUauso/2AssuLobQj85iUTBCLK6YJbUP7AxfFwXHjKn3F3URNgh\n/2SvLbPOPqS3wos0B9h8m/Q9jtfCVvEt3TqiLY0dSeff12q/2BmDRKpBQYHFeO+F8UKtMuNdoA==\n ",
"hashTheKeyInfo": {
"keyPlaintext": "A45BE7EF0543D36B4F68DvD3EF4013D4",
"keyHashed": "zb6XIjgHtuii5dEcQuiY/8QFluA\n"
},
"SKFGetInstance": {
"secretKeyType": "PBKDF2WithHmacSHA1",
"providerName": "BC",
"providerVersion": 1.77,
"providerInfo": "BouncyCastle Security Provider v1.77"
},
"SKFGenerateSecret": {
"algorithm": "PBKDF2WithHmacSHA1",
"PBEKeySpecInfo": {
"iterationCount": 1,
"keyLength": 128,
"password": "zb6XIjgHtuii5dEcQuiY/8QFluA\n",
"salt": "BDBA982FAF389694"
},
"secretKey": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c"
},
"SKSInit": {
"key": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c",
"Algorithm": "AES"
},
"getSecretKeyInfo": {
"algorithm": "AES",
"keyMaterial": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c"
},
"cryptoInitInfo": {
"opmode": 2,
"opmodeString": "DECRYPT",
"key": {
"keyClass": "javax.crypto.spec.SecretKeySpec",
"keyAlgorithm": "AES",
"keyFormat": "RAW",
"keyEncoded": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c"
},
"parameters": {
"parameterClass": "javax.crypto.spec.IvParameterSpec",
"parameterValue": "00000000000000000000000000000000"
},
"secureRandom": {
"secureRandomClass": "java.security.SecureRandom",
"secureRandomAlgorithm": "SHA1PRNG",
"secureRandomProvider": "AndroidOpenSSL version 1.0"
}
},
"decrypted": {
"commonPrefs": {
"display_rate_us_after": 0,
"permission_event_severity": "event"
},
"devicePrefs": {}
}
}
Secret key for both encrypt and decrypt operations is built upon a hardcoded key in hashTheKey method:

As an example, let's take Encryption.encrypt() method:

This method works roughly as follows:
getSecretKey method is prepared by hashTheKey method:
utf-8).digest() method, the final hash being encoded to Base64 string (with NO_PADDING flag enabled) and converted to a char array.getSecretKey method instantiates SecretKeyFactory object:
PBKDF2WithHmacSHA1.PBEKeySpec is instantiated with the following parameters:
hashTheKey method (PBKDF2 password represented as a Java character array).generateSecret method computes secret key from the previously listed parameters.Cipher object is instantiated with the transformation AES/CBC/PKCS5Padding and is initialized with:
Cipher.doFinal().Encryption.decrypt() method follows the same Cipher pattern, just with opmode = 2:

Javascript code for this Frida overload is available in this repository as perenio.js. Python reimplementation of the described encryption method is available at pythonImplementations/crypto.py.
Sample outputs of dynamic analysis script can be found in sampleLogs directory
A few notable considerations regarding the cryptographic methods used in the Perenio Android application:
Note: This is an app-only encryption method. Data in-transit is still being guarded by TLSv1.3, as well as SSL certificates pinned within Perenio Android application.
Given the points listed above, the in-app security layer reads more like an obfuscation layer rather than true random, non-deterministic encryption method.
Login sequence was intercepted using Mitmproxy and captured with Wireshark for analysis. However, network communications between Perenio Android application and the vendor's remote servers have a couple of protections:
Tricking Android device into trusting my proxy for establishing TLSv1.3 session with it is as simple as injecting Mitmproxy's CA certificates onto Android system trust store (Bash automation script for this process can be found here). For pinned certificate bypass, Frida objection with android sslpinning disable script was used.
Note: For dynamic analysis with Frida, frida-server must be loaded and run on a target device as a daemon, so that a Frida client on a host machine could have a "harness" to be able to attach to a process and inspect/modify it.
A quick description of the algorithm used for capturing plain text communication between Perenio Android app and vendor's servers might be this:
frida-server-<version>-android-arm64 onto a rooted Android device or an emulator via adb.frida-server on Android device.uv run frida-ps -U | grep -i perenio.uv run objection -g "${pid}" explore.android sslpinning disable script.SSLKEYLOGFILE=/path/to/key/log/file mitmproxy --listen-port <PORT> --ssl-insecure. Saving SSL keys onto a file is necessary, because, even if Wireshark and Mitmproxy run concurrently, Wireshark does not log these keys itself. And without these keys, Wireshark can not decrypt the captured TLS sessions.A more comprehensive guide on HTTPS proxy setup and SSL pinning bypass can be found here.
The method described above, the following plain text login request was captured:
:method: POST
:scheme: https
:path: /auth/realms/aaa.kaa/protocol/openid-connect/token
:authority: oauth.perenio.com
tenantid: perenio
content-type: application/x-www-form-urlencoded
content-length: 144
accept-encoding: gzip
user-agent: okhttp/4.10.0
username=<email>&password=<account-password>&grant_type=password&client_id=perenio-app&client_secret=<client-secret>
.............:status: 200
date: Tue, 15 Sep 2026 14:59:55 GMT
content-type: application/json
vary: Accept-Encoding
cache-control: no-store
set-cookie: KEYCLOAK_LOCALE=; Version=1; Comment=Expiring cookie; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/aaa.kaa/; Secure; HttpOnly
set-cookie: KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/aaa.kaa/; Secure; HttpOnly
x-xss-protection: 1; mode=block
pragma: no-cache
x-frame-options: SAMEORIGIN
strict-transport-security: max-age=15724800; includeSubDomains
x-content-type-options: nosniff
content-encoding: gzip
{"access_token":"<access-token>","expires_in":864000,"refresh_expires_in":51840000,"refresh_token":"<refresh-token>","token_type":"bearer","not-before-policy":0,"session_state":"<session-state>","scope":"profile email"}
A few insights from the login request:
Heading back to the decompiled Perenio application, the login function is implemented in com.perenio.data.datasource.sections.auth.AuthRepository function:

With this information obtained from Wireshark packet capture, I decided to write a proof-of-concept Python script that POSTs a login request to an endpoint controlled by Perenio, receives an authorization token and queries hxxps[://]oauth[.]perenio[.com/auth/realms/aaa[.]kaa/users/me endpoint to get user information of my account. The script is called sampleRequest.py and can be found in pythonImplementations folder.
Script also queries a third endpoint - registrationStatus - to check whether an account with a given email address has been registered.
Response if account exists:
{
"status": "EXISTS"
}
Response if account does not exist:
{
"status": "NOT_EXISTS"
}
I have not tested the rate limits on this endpoint, but it potentially could be used to enumerate existing Perenio accounts.
Sample response to login request:
{
"access_token": "<access-token>",
"expires_in": 864000,
"refresh_expires_in": 51840000,
"refresh_token": "<refresh-token>",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "<session-state>",
"scope": "profile email"
}
Sample response to user information query:
{
"uid": "<user-id>",
"countryCode": "LT",
"email": "<email>",
"language": "EN",
"nickname": "<nickname>"
}
This also further proves that encryption methods described previously are local-only as these HTTP requests and responses contain plain text body - the encryption aspect is handed to TLSv1.3 and pinned SSL certificates.
The script can also be found as sampleRequest.py in pythonImplementations folder
Perenio Peifc01 IP camera captures and transmits both audio and video streams. A sample stream was collected using Comfast CF-922AC USB Wi-Fi adapter in monitor mode.
Packet collection process can be described as follows:
ip a
nmcli device wifi list
sudo airmon-ng start <comfast-interface> <channel-number>
<comfast-interface>mon interface.Wireshark shows heavy UDP traffic on ports 50612, 54386 and 54431:

The datagrams themselves seem to contain highly structured data - decoding them as Real-time Transport Protocol (RTP) on Wireshark with Right click on UDP packet -> Decode as -> RTP reveals that the captured UDP data is audio/video stream relay between Perenio Peifc01 IP camera and a smartphone via vendor controlled Amazon AWS EC2 instance. The following structure can be inferred from this packet capture:
| Port number | Protocol | Purpose |
|---|---|---|
| 50612 | RTP over UDP | Audio stream IP camera -> EC2 instance |
| 54386 | RTP over UDP | Video stream IP camera -> EC2 instance |
| 54431 | SRTP over UDP | Stream relay EC2 instance -> smartphone |
IP camera sends video/audio streams as plaintext RTP data over the network to an Amazon AWS EC2 instance, controlled by the vendor. EC2 instance then relays combined audio/video stream to Android application via SRTP (Secure RTP) channel.
RTP header, as per RFC3550 specification, has the following structure:
Visual RTP header representation from RFC3550 specification is presented below:

After a fixed header and CSRC identifiers (if present, RTP packet can have 0 to 15 32-bit long items as CSRC list entries right after SSRC), payload is added to the RTP packet. If padding is set to 1, a certain amount of padding bytes are appended to the packet after payload. Count of padding bytes is stored as a last byte of RTP packet (count includes this last byte as well).
As a video stream codec, H.264 (also known as Advanced Video Coding - AVC) is used. When a video is encoded with a standard like H.264, the stream is sliced into Network Abstraction Layer (NAL) units for reliable data transmission over a network. However, RTP over UDP has a limit for how large a single network packet can be. This limit is called Maximum Transmission Unit (MTU) and for Ethernet/Wi-Fi networks it usually is 1500 bytes, including packet headers. Accounting for Ethernet/IPv4/UDP/RTP headers leaves 1460 bytes at most for the payload (additional header information is added to RTP packet if the packet has CSRC identifiers and/or is being sent via VPN/IPSec tunnel).
Anyways, a large NAL unit can exceed single MTU, which means that such a NAL unit has to be split into multiple RTP packets for a successful transmission. For this purpose, H.264 has a defined Fragmentation Unit (FU), comprised of 1-byte FU Indicator and 1-byte FU Header. NAL header is reconstructed with Original NAL Header Byte = (FU Indicator & 0xE0) | (FU Header & 0x1F)
FU Indicator is constructed against RFC 6184 standard, section 1.3:
| Bits | Name | Description |
|---|---|---|
| 0 | F | forbidden_zero_bit |
| 1-2 | NRI | nal_ref_idc |
| 3-7 | Type | nal_unit_type |
28.Visual FU Indicator structure from RFC 6184 standard, section 1.3 is presented below:

FU Header byte is present only on NAL Fragmentation Units and is constructed against RFC 6184 standard, section 5.8
| Bits | Name | Description |
|---|---|---|
| 0 | S | Start bit of fragmented NAL unit |
| 1 | E | End bit of fragmented NAL unit |
| 2 | R | Reserved bit - must equal 0 and ignored by the receiver |
| 3-7 | Type | NAL unit type |
Visual FU Header structure from RFC 6184 standard, section 5.8 is presented below:

When a NAL unit is fragmented into FU-A (and FU-B) units, the original one-byte NAL header is reconstructed by preserving the F and NRI bits from the FU indicator and taking the NAL-unit type from the FU header
(FU Identifier & 0xE0) | (FU Header & 0x1F)
This operation swaps fragmentation unit type (28 for FU-A and 29 for FU-B) with the type of actual video data in NALU.
Stream reconstruction script is available as streamExtraction.py script in pythonImplementations folder.
As of now, only the relevant features for Perenio Peifc01 IP camera's stream reconstruction are included in this script:
.wav audio file.Features that are not included in the script:
The script, however, produces playable audio and video files, with a reconstructed video frame presented below:

Key points to summarize the security research of Perenio application:
Perenio Smart: Home and Office Android application revealed a few issues with how local data encryption is implemented.accessToken that is valid for 10 days is issued while the validity period of refreshToken is 600 days.5 commits
Python
60.4%
JavaScript
39.6%
Analysis of cryptographic routines found within "Perenio Smart: Home and Office" Android application, as well as reconstructing Perenio Peifc01 IP camera's RTP over UDP video stream from a Wireshark packet capture
Python
0
5 commits
updated Sep 24, 2026
Analysis of cryptographic routines found within "Perenio Smart: Home and Office" Android application, as well as reconstructing Perenio Peifc01 IP camera's RTP over UDP video stream from a Wireshark packet capture
Using my custom Frida script for cryptographic method detection in Android applications, I captured Java Cipher class doing a decryption operation, along with it's stack trace:
{
"objectId": "Cipher-30236645",
"timestamp": 1789412643454,
"lastSeen": 1789412643488,
"instanceOverload": "1. [Cipher.getInstance(java.lang.String) -> static Cipher]",
"transformation": "AES/CBC/PKCS5Padding",
"algorithm": "AES/CBC/PKCS5Padding",
"runtimeClass": "javax.crypto.Cipher",
"providerName": "AndroidOpenSSL",
"providerVersion": 1,
"providerInfo": "Android's OpenSSL-backed security provider",
"providerClass": "com.android.org.conscrypt.OpenSSLProvider",
"updateInputs": [],
"updateInputsLen": [],
"updateOutputs": [],
"updateOutputsLen": [],
"initOverload": "6. [Cipher.init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) -> void]",
"opmode": 2,
"opmodeString": "DECRYPT",
"blockSize": 16,
"keyClass": "javax.crypto.spec.SecretKeySpec",
"keyAlgorithm": "AES",
"keyFormat": "RAW",
"keyBytesHex": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c",
"keyBytesString": "K.N.]{.........\\",
"keyBytesFingerprint": "32862b80:16",
"parameterClass": "javax.crypto.spec.IvParameterSpec",
"iv": "000000000000000000000000000000",
"ivFingerprint": "69691905:16",
"secureRandom": "java.security.SecureRandom",
"finalOverload": "2. [Cipher.doFinal(byte[] input) -> byte[]]",
"bytesWritten": 96,
"input": "7c354a11ebcff3abe4b3c1c359cb16c14a1685851abaca3fd80b2cb8ba1b423f398944c108b2ba6096d43fb0317c5c17 ... [64 more bytes]",
"output": "{\"commonPrefs\":{\"display_rate_us_after\":0,\"permi ... [48 more bytes]",
"outputFingerprint": "a29ad7a9:96",
"stackTrace": [
"java.lang.Throwable",
"at javax.crypto.Cipher.doFinal(Native Method)",
"at se.simbio.encryption.Encryption.decrypt(Encryption.java:179)",
"at se.simbio.encryption.Encryption.decryptOrNull(Encryption.java:193)",
"at com.perenio.data.util.EncryptionUtils.decrypt(EncryptionUtils.kt:21)",
"at com.perenio.data.local.sharedpreference.base.AbsEncryptPreferencesProvider.getStringDecrypt(AbsEncryptPreferencesProvider.kt:15)",
"at com.perenio.data.local.sharedpreference.base.AbsEncryptPreferencesProvider.getStringDecrypt$default(AbsEncryptPreferencesProvider.kt:13)",
"at com.perenio.data.local.sharedpreference.SharedPreferencesProvider.<init>(SharedPreferencesProvider.kt:149)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule.provideSharedPreferencesProvider(SharedPreferencesModule.kt:25)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.provideSharedPreferencesProvider(SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.java:56)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.get(SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.java:44)",
"at com.perenio.smarthome.di.module.data.SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.get(SharedPreferencesModule_ProvideSharedPreferencesProviderFactory.java:14)",
"at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)",
"at com.perenio.smarthome.di.module.data.SectionsModule_ProvideUserSectionFactory.get(SectionsModule_ProvideUserSectionFactory.java:68)",
"at com.perenio.smarthome.di.module.data.SectionsModule_ProvideUserSectionFactory.get(SectionsModule_ProvideUserSectionFactory.java:19)",
"at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)",
"at com.perenio.smarthome.di.component.DaggerAppComponent$AppComponentImpl.synchronizeSubscriptionsUseCase(DaggerAppComponent.java:2416)",
"at com.perenio.smarthome.di.component.DaggerAppComponent$AppComponentImpl.injectPerenioApp(DaggerAppComponent.java:3815)",
"at com.perenio.smarthome.di.component.DaggerAppComponent$AppComponentImpl.inject(DaggerAppComponent.java:3036)",
"at com.perenio.smarthome.PerenioApp.onCreate(PerenioApp.kt:47)",
"at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1385)",
"at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7830)",
"at android.app.ActivityThread.handleBindApplication(Native Method)",
"at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)",
"at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2546)",
"at android.os.Handler.dispatchMessage(Handler.java:110)",
"at android.os.Looper.loopOnce(Looper.java:248)",
"at android.os.Looper.loop(Looper.java:338)",
"at android.app.ActivityThread.main(ActivityThread.java:9068)",
"at java.lang.reflect.Method.invoke(Native Method)",
"at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:596)",
"at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932)\n"
]
}
Name of the Android application is com.perenio.smarthome.
adb shell pm list packages | grep <package-name>
adb shell pm path <package-name>:package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/base.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_asset_pack.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_config.arm64_v8a.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_config.en.apk
package:/data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/split_config.xxhdpi.apk
Core application logic is stored in base.apk with assets and configs stored in separate .apk files. For the purposes of current research, only the base application file was analyzed.
base.apk to host machine:adb pull /data/app/~~s-<base64-string>/<package-name>-n_K_-<base64-string>/base.apk
This section covers analysis of Perenio application's data encryption and decryption methods. Both static analysis with JADX and dynamic analysis using Frida were performed.
One important thing to note: the described encryption method is a local-only encryption method, i.e. it only protects data that is at rest in local storage. Data transmitted via network is protected by TLSv1.3 sessions and SSL certificates pinned into Perenio app.
Searching for se.simbio.encryption.Encryption in Jadx-gui leads to the decompiled code of the Encryption class (whose scope is set to public).
The class has getDefault(String str, String str2, byte[] bArr) { ... } method that creates an object of Builder class, responsible for providing information necessary to construct key and initialization vector (IV) for AES encryption:

This method is called from com.perenio.data.util.EncryptionUtils class and gets hardcoded key and salt values provided as arguments:

The builder method itself builds a key from provided information and has setters and getters for every private variable:

Overloading the getDefaultBuilder method (along with decrypt method) using Frida discloses that the following information is being received during Perenio application's initialization sequence:
During the app initialization stage, Frida overloads for getDefaultBuilder and decrypt methods captured the following information about building a key and IV for AES decryption operation:
{
"threadId": "2",
"builder": {
"algorithm": "AES/CBC/PKCS5Padding",
"modeBase64": 0,
"charsetName": "UTF8",
"digestAlgorithm": "SHA1",
"iterationCount": 1,
"iv": "00000000000000000000000000000000",
"ivParameterSpec": null,
"key": "A45BE7EF0543D36B4F68DvD3EF4013D4",
"keyAlgorithm": "AES",
"keyLength": 128,
"salt": "BDBA982FAF389694",
"secretKeyType": "PBKDF2WithHmacSHA1",
"secureRandom": null,
"secureRandomAlgorithm": "SHA1PRNG"
},
"getDefaultInfo": {
"key": "A45BE7EF0543D36B4F68DvD3EF4013D4",
"salt": "BDBA982FAF389694",
"defaultKey": "00000000000000000000000000000000"
},
"encrypted": "fDVKEevP86vks8HDWcsWwUoWhYUauso/2AssuLobQj85iUTBCLK6YJbUP7AxfFwXHjKn3F3URNgh\n/2SvLbPOPqS3wos0B9h8m/Q9jtfCVvEt3TqiLY0dSeff12q/2BmDRKpBQYHFeO+F8UKtMuNdoA==\n ",
"hashTheKeyInfo": {
"keyPlaintext": "A45BE7EF0543D36B4F68DvD3EF4013D4",
"keyHashed": "zb6XIjgHtuii5dEcQuiY/8QFluA\n"
},
"SKFGetInstance": {
"secretKeyType": "PBKDF2WithHmacSHA1",
"providerName": "BC",
"providerVersion": 1.77,
"providerInfo": "BouncyCastle Security Provider v1.77"
},
"SKFGenerateSecret": {
"algorithm": "PBKDF2WithHmacSHA1",
"PBEKeySpecInfo": {
"iterationCount": 1,
"keyLength": 128,
"password": "zb6XIjgHtuii5dEcQuiY/8QFluA\n",
"salt": "BDBA982FAF389694"
},
"secretKey": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c"
},
"SKSInit": {
"key": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c",
"Algorithm": "AES"
},
"getSecretKeyInfo": {
"algorithm": "AES",
"keyMaterial": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c"
},
"cryptoInitInfo": {
"opmode": 2,
"opmodeString": "DECRYPT",
"key": {
"keyClass": "javax.crypto.spec.SecretKeySpec",
"keyAlgorithm": "AES",
"keyFormat": "RAW",
"keyEncoded": "4bf14ea15d7bd3bbfcf9b7d9acc6db5c"
},
"parameters": {
"parameterClass": "javax.crypto.spec.IvParameterSpec",
"parameterValue": "00000000000000000000000000000000"
},
"secureRandom": {
"secureRandomClass": "java.security.SecureRandom",
"secureRandomAlgorithm": "SHA1PRNG",
"secureRandomProvider": "AndroidOpenSSL version 1.0"
}
},
"decrypted": {
"commonPrefs": {
"display_rate_us_after": 0,
"permission_event_severity": "event"
},
"devicePrefs": {}
}
}
Secret key for both encrypt and decrypt operations is built upon a hardcoded key in hashTheKey method:

As an example, let's take Encryption.encrypt() method:

This method works roughly as follows:
getSecretKey method is prepared by hashTheKey method:
utf-8).digest() method, the final hash being encoded to Base64 string (with NO_PADDING flag enabled) and converted to a char array.getSecretKey method instantiates SecretKeyFactory object:
PBKDF2WithHmacSHA1.PBEKeySpec is instantiated with the following parameters:
hashTheKey method (PBKDF2 password represented as a Java character array).generateSecret method computes secret key from the previously listed parameters.Cipher object is instantiated with the transformation AES/CBC/PKCS5Padding and is initialized with:
Cipher.doFinal().Encryption.decrypt() method follows the same Cipher pattern, just with opmode = 2:

Javascript code for this Frida overload is available in this repository as perenio.js. Python reimplementation of the described encryption method is available at pythonImplementations/crypto.py.
Sample outputs of dynamic analysis script can be found in sampleLogs directory
A few notable considerations regarding the cryptographic methods used in the Perenio Android application:
Note: This is an app-only encryption method. Data in-transit is still being guarded by TLSv1.3, as well as SSL certificates pinned within Perenio Android application.
Given the points listed above, the in-app security layer reads more like an obfuscation layer rather than true random, non-deterministic encryption method.
Login sequence was intercepted using Mitmproxy and captured with Wireshark for analysis. However, network communications between Perenio Android application and the vendor's remote servers have a couple of protections:
Tricking Android device into trusting my proxy for establishing TLSv1.3 session with it is as simple as injecting Mitmproxy's CA certificates onto Android system trust store (Bash automation script for this process can be found here). For pinned certificate bypass, Frida objection with android sslpinning disable script was used.
Note: For dynamic analysis with Frida, frida-server must be loaded and run on a target device as a daemon, so that a Frida client on a host machine could have a "harness" to be able to attach to a process and inspect/modify it.
A quick description of the algorithm used for capturing plain text communication between Perenio Android app and vendor's servers might be this:
frida-server-<version>-android-arm64 onto a rooted Android device or an emulator via adb.frida-server on Android device.uv run frida-ps -U | grep -i perenio.uv run objection -g "${pid}" explore.android sslpinning disable script.SSLKEYLOGFILE=/path/to/key/log/file mitmproxy --listen-port <PORT> --ssl-insecure. Saving SSL keys onto a file is necessary, because, even if Wireshark and Mitmproxy run concurrently, Wireshark does not log these keys itself. And without these keys, Wireshark can not decrypt the captured TLS sessions.A more comprehensive guide on HTTPS proxy setup and SSL pinning bypass can be found here.
The method described above, the following plain text login request was captured:
:method: POST
:scheme: https
:path: /auth/realms/aaa.kaa/protocol/openid-connect/token
:authority: oauth.perenio.com
tenantid: perenio
content-type: application/x-www-form-urlencoded
content-length: 144
accept-encoding: gzip
user-agent: okhttp/4.10.0
username=<email>&password=<account-password>&grant_type=password&client_id=perenio-app&client_secret=<client-secret>
.............:status: 200
date: Tue, 15 Sep 2026 14:59:55 GMT
content-type: application/json
vary: Accept-Encoding
cache-control: no-store
set-cookie: KEYCLOAK_LOCALE=; Version=1; Comment=Expiring cookie; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/aaa.kaa/; Secure; HttpOnly
set-cookie: KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/aaa.kaa/; Secure; HttpOnly
x-xss-protection: 1; mode=block
pragma: no-cache
x-frame-options: SAMEORIGIN
strict-transport-security: max-age=15724800; includeSubDomains
x-content-type-options: nosniff
content-encoding: gzip
{"access_token":"<access-token>","expires_in":864000,"refresh_expires_in":51840000,"refresh_token":"<refresh-token>","token_type":"bearer","not-before-policy":0,"session_state":"<session-state>","scope":"profile email"}
A few insights from the login request:
Heading back to the decompiled Perenio application, the login function is implemented in com.perenio.data.datasource.sections.auth.AuthRepository function:

With this information obtained from Wireshark packet capture, I decided to write a proof-of-concept Python script that POSTs a login request to an endpoint controlled by Perenio, receives an authorization token and queries hxxps[://]oauth[.]perenio[.com/auth/realms/aaa[.]kaa/users/me endpoint to get user information of my account. The script is called sampleRequest.py and can be found in pythonImplementations folder.
Script also queries a third endpoint - registrationStatus - to check whether an account with a given email address has been registered.
Response if account exists:
{
"status": "EXISTS"
}
Response if account does not exist:
{
"status": "NOT_EXISTS"
}
I have not tested the rate limits on this endpoint, but it potentially could be used to enumerate existing Perenio accounts.
Sample response to login request:
{
"access_token": "<access-token>",
"expires_in": 864000,
"refresh_expires_in": 51840000,
"refresh_token": "<refresh-token>",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "<session-state>",
"scope": "profile email"
}
Sample response to user information query:
{
"uid": "<user-id>",
"countryCode": "LT",
"email": "<email>",
"language": "EN",
"nickname": "<nickname>"
}
This also further proves that encryption methods described previously are local-only as these HTTP requests and responses contain plain text body - the encryption aspect is handed to TLSv1.3 and pinned SSL certificates.
The script can also be found as sampleRequest.py in pythonImplementations folder
Perenio Peifc01 IP camera captures and transmits both audio and video streams. A sample stream was collected using Comfast CF-922AC USB Wi-Fi adapter in monitor mode.
Packet collection process can be described as follows:
ip a
nmcli device wifi list
sudo airmon-ng start <comfast-interface> <channel-number>
<comfast-interface>mon interface.Wireshark shows heavy UDP traffic on ports 50612, 54386 and 54431:

The datagrams themselves seem to contain highly structured data - decoding them as Real-time Transport Protocol (RTP) on Wireshark with Right click on UDP packet -> Decode as -> RTP reveals that the captured UDP data is audio/video stream relay between Perenio Peifc01 IP camera and a smartphone via vendor controlled Amazon AWS EC2 instance. The following structure can be inferred from this packet capture:
| Port number | Protocol | Purpose |
|---|---|---|
| 50612 | RTP over UDP | Audio stream IP camera -> EC2 instance |
| 54386 | RTP over UDP | Video stream IP camera -> EC2 instance |
| 54431 | SRTP over UDP | Stream relay EC2 instance -> smartphone |
IP camera sends video/audio streams as plaintext RTP data over the network to an Amazon AWS EC2 instance, controlled by the vendor. EC2 instance then relays combined audio/video stream to Android application via SRTP (Secure RTP) channel.
RTP header, as per RFC3550 specification, has the following structure:
Visual RTP header representation from RFC3550 specification is presented below:

After a fixed header and CSRC identifiers (if present, RTP packet can have 0 to 15 32-bit long items as CSRC list entries right after SSRC), payload is added to the RTP packet. If padding is set to 1, a certain amount of padding bytes are appended to the packet after payload. Count of padding bytes is stored as a last byte of RTP packet (count includes this last byte as well).
As a video stream codec, H.264 (also known as Advanced Video Coding - AVC) is used. When a video is encoded with a standard like H.264, the stream is sliced into Network Abstraction Layer (NAL) units for reliable data transmission over a network. However, RTP over UDP has a limit for how large a single network packet can be. This limit is called Maximum Transmission Unit (MTU) and for Ethernet/Wi-Fi networks it usually is 1500 bytes, including packet headers. Accounting for Ethernet/IPv4/UDP/RTP headers leaves 1460 bytes at most for the payload (additional header information is added to RTP packet if the packet has CSRC identifiers and/or is being sent via VPN/IPSec tunnel).
Anyways, a large NAL unit can exceed single MTU, which means that such a NAL unit has to be split into multiple RTP packets for a successful transmission. For this purpose, H.264 has a defined Fragmentation Unit (FU), comprised of 1-byte FU Indicator and 1-byte FU Header. NAL header is reconstructed with Original NAL Header Byte = (FU Indicator & 0xE0) | (FU Header & 0x1F)
FU Indicator is constructed against RFC 6184 standard, section 1.3:
| Bits | Name | Description |
|---|---|---|
| 0 | F | forbidden_zero_bit |
| 1-2 | NRI | nal_ref_idc |
| 3-7 | Type | nal_unit_type |
28.Visual FU Indicator structure from RFC 6184 standard, section 1.3 is presented below:

FU Header byte is present only on NAL Fragmentation Units and is constructed against RFC 6184 standard, section 5.8
| Bits | Name | Description |
|---|---|---|
| 0 | S | Start bit of fragmented NAL unit |
| 1 | E | End bit of fragmented NAL unit |
| 2 | R | Reserved bit - must equal 0 and ignored by the receiver |
| 3-7 | Type | NAL unit type |
Visual FU Header structure from RFC 6184 standard, section 5.8 is presented below:

When a NAL unit is fragmented into FU-A (and FU-B) units, the original one-byte NAL header is reconstructed by preserving the F and NRI bits from the FU indicator and taking the NAL-unit type from the FU header
(FU Identifier & 0xE0) | (FU Header & 0x1F)
This operation swaps fragmentation unit type (28 for FU-A and 29 for FU-B) with the type of actual video data in NALU.
Stream reconstruction script is available as streamExtraction.py script in pythonImplementations folder.
As of now, only the relevant features for Perenio Peifc01 IP camera's stream reconstruction are included in this script:
.wav audio file.Features that are not included in the script:
The script, however, produces playable audio and video files, with a reconstructed video frame presented below:

Key points to summarize the security research of Perenio application:
Perenio Smart: Home and Office Android application revealed a few issues with how local data encryption is implemented.accessToken that is valid for 10 days is issued while the validity period of refreshToken is 600 days.5 commits
Python
60.4%
JavaScript
39.6%