{"id":27286286,"url":"https://github.com/rec0de/glyph-api","last_synced_at":"2025-08-09T19:13:06.941Z","repository":{"id":189034240,"uuid":"679900808","full_name":"rec0de/glyph-api","owner":"rec0de","description":"Documentation of the as yet unofficial Glyph Light API on Nothing OS","archived":false,"fork":false,"pushed_at":"2024-02-22T11:43:06.000Z","size":32,"stargazers_count":43,"open_issues_count":1,"forks_count":1,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-11T19:46:44.181Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rec0de.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-08-17T22:04:22.000Z","updated_at":"2025-01-14T19:28:16.000Z","dependencies_parsed_at":"2023-08-18T00:31:00.222Z","dependency_job_id":"6f9c4e64-9816-4448-808b-41d306b3f6a0","html_url":"https://github.com/rec0de/glyph-api","commit_stats":null,"previous_names":["rec0de/glyph-api"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rec0de/glyph-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rec0de%2Fglyph-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rec0de%2Fglyph-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rec0de%2Fglyph-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rec0de%2Fglyph-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rec0de","download_url":"https://codeload.github.com/rec0de/glyph-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rec0de%2Fglyph-api/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264446721,"owners_count":23609633,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-11T19:35:47.653Z","updated_at":"2025-07-09T11:07:29.434Z","avatar_url":"https://github.com/rec0de.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Glyph API\n\n_How you could use the glyph lights on your nothing phone, and why you can't, and how you maybe could anyway?_\n\n**Update: Nothing has released [official documentation of the Glyph API](https://github.com/Nothing-Developer-Programme/Glyph-Developer-Kit) and the API key application process appears to open up soon.**\n\nIt seems there is not a lot of information out there on how the glyph lights on a nothing phone (i'll be talking about the phone 2, but this should largely also apply to the phone 1) work and how they can be controlled by third-party, i.e. non-root apps. We just know that it has to work _somehow_ because the Glyph Composer app is able to do so. So I did a little reversing to shed some light:\n\n# Theory\n\nInternally, the lights are controlled using the [Android Hardware Lights Service](https://developer.android.com/reference/android/hardware/lights/LightsManager). Using this service requires the `CONTROL_DEVICE_LIGHTS_PERMISSION`, which is only granted to system apps. So that's a dead end.\n\nSo how does the glyph composer do it?\n\nLooking into a decompiled APK, we can find an interesting permission:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cmanifest\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:versionCode=\"1030000\"\n    android:versionName=\"1.3.0\"\n    android:compileSdkVersion=\"33\"\n    android:compileSdkVersionCodename=\"13\"\n    package=\"com.nothing.glyph.composer\"\n    platformBuildVersionCode=\"33\"\n    platformBuildVersionName=\"13\"\u003e\n\n    ...\n\n    \u003cuses-permission\n        android:name=\"com.nothing.ketchum.permission.ENABLE\" /\u003e\n    ...\n\n\u003c/manifest\u003e\n```\n\nDiving a bit deeper into the decompiled code, we find that the app binds to a promising looking service:\n\n```java\npublic final void b() {\n  Object m7;\n        r3.a aVar = this.f2457d;\n        aVar.getClass();\n        try {\n            if (aVar.f6404b == 0) {\n                s3.a b7 = aVar.b();\n                b7.getClass();\n                Intent intent = new Intent();\n                intent.setPackage(\"com.nothing.thirdparty\");\n                intent.setAction(\"com.nothing.thirdparty.bind_glyphservice\");\n                intent.setComponent(new ComponentName(\"com.nothing.thirdparty\", \"com.nothing.thirdparty.GlyphService\"));\n                b7.f6730a.bindService(intent, b7.f6731b, 1);\n                Log.i(\"GlyphComposer_GlyphManagerCompat\", \"init\");\n            }\n            int i7 = aVar.f6404b;\n            aVar.f6404b = i7 + 1;\n            m7 = Integer.valueOf(i7);\n        } catch (Throwable th) {\n            m7 = e1.m(th);\n        }\n        Throwable a7 = h4.d.a(m7);\n        if (a7 != null) {\n            String str = \"init error:\" + a7;\n            h.e(str, \"message\");\n            Log.e(\"GlyphComposer_GlyphManagerCompat\", str);\n        }\n}\n```\n\nThe interface for this service looks rather simple, and after a little reversing it boils down to this:\n\n```kotlin\ninterface GlyphInterface : IInterface {\n    fun setFrameColors(iArr: IntArray)\n    fun closeSession()\n    fun openSession()\n    fun register(str: String): Boolean\n}\n```\n\nLooks pretty straightforward, right? You connect to the service, call `openSession` and then control the lights using `setFrameColor`, which I strongly suspect takes 33 brightness values for each of the addressable zones — I'll put a very minimal example on how you would use this in an app at the bottom.\n\n# Obstacles\n\nThere's just one problem: Nothing doesn't want you to do that. Let's have a look at the `com.nothing.thirdparty` package to see what's going on. Here's a condensed version of the `GlyphService.java` file that is essentially the 'other end' of the `GlyphInterface` we've seen in the composer.\n\n```java\npublic class GlyphService extends Service {\n    private static final boolean DBG = Def.DBG;\n    private Context mContext;\n    private LightsManager mLightsManager;\n    private AuthController mAuth = null;\n    private GlyphReceiver mGlyphReciever = null;\n    private GlyphAdapter mAdapter = null;\n    private AuthController.Callback mAuthCallback = null;\n    private GlyphReceiver.Callback mGlyphReceiverCallback = null;\n    private String mCurrentFocusPkg = null;\n    private HashMap\u003cInteger, LightsManager.LightsSession\u003e mSessionMap = new HashMap\u003c\u003e();\n    private HashMap\u003cInteger, String\u003e mUidPkgMap = new HashMap\u003c\u003e();\n    private IGlyphService.Stub mStub = new IGlyphService.Stub() { \n\n        public boolean register(String str) throws RemoteException {\n            if (GlyphService.DBG) {\n                Log.d(\"GlyphService\", \"register\");\n            }\n            return GlyphService.this.mAuth.register(Utils.getCallingPackageName(GlyphService.this.mContext), str, Utils.getCallingUid());\n        }\n\n        public void openSession() throws RemoteException {\n            boolean allowInBackground;\n            synchronized (GlyphService.this.mSessionMap) {\n                String callingPackageName = Utils.getCallingPackageName(GlyphService.this.mContext);\n                int callingUid = Utils.getCallingUid();\n                int i = 115;\n                boolean authorized = true;\n                if (\"com.nothing.glyph.composer\".equals(callingPackageName) \u0026\u0026 Utils.checkFingerprint(GlyphService.this.mContext, callingPackageName)) {\n                    GlyphService.this.mAuth.addAlreadyAuth(callingPackageName, callingUid);\n                    i = 110;\n                    allowInBackground = true;\n                } else {\n                    allowInBackground = false;\n                }\n                if (callingUid == 1000) {\n                    allowInBackground = true;\n                }\n                if (!GlyphService.this.mUidPkgMap.containsKey(callingUid)) {\n                    GlyphService.this.mUidPkgMap.put(callingUid, callingPackageName);\n                }\n                if (GlyphService.this.mAuth.checkAlreadyAuth(GlyphService.this.mUidPkgMap.get(callingUid))) {\n                    if (!GlyphService.this.mAuth.checkForeground(callingPackageName)) {\n                        authorized = allowInBackground;\n                    }\n                    if (authorized) {\n                        if (GlyphService.this.mSessionMap.get(i) == null) {\n                            GlyphService.this.mSessionMap.put(i, GlyphService.this.mLightsManager.openSession());\n                            if (GlyphService.DBG) {\n                                Log.d(\"GlyphService\", \"openSession:\" + callingPackageName);\n                            }\n                        } else if (GlyphService.DBG) {\n                            Log.d(\"GlyphService\", \"already openSession\");\n                        }\n                    } else {\n                        Log.e(\"GlyphService\", \"Fail to connect.\");\n                    }\n                }\n            }\n        }\n        \n        public void closeSession() throws RemoteException { ... }\n\n        public void setFrameColors(int[] iArr) throws RemoteException {\n            String str = GlyphService.this.mUidPkgMap.getOrDefault(Utils.getCallingUid(), null);\n            GlyphService.this.mCurrentFocusPkg = str;\n            LightsManager.LightsSession lightsSession = GlyphService.this.mSessionMap.getOrDefault(Integer.valueOf(\"com.nothing.glyph.composer\".equals(str) ? 110 : 115), null);\n            if (str == null) {\n                Log.e(\"GlyphService\", \"pkg is null\");\n            } else if (lightsSession == null) {\n                Log.e(\"GlyphService\", \"session is null\");\n            } else if (!GlyphService.this.mAuth.checkAlreadyAuth(str)) {\n                Log.e(\"GlyphService\", \"Non register\");\n            } else if (!GlyphService.this.mAuth.checkForeground(str)) {\n                GlyphService.this.resetFrameColor(lightsSession);\n                Log.e(\"GlyphService\", str + \" is not foreground\");\n            } else {\n                GlyphService.this.setFrameColorsInner(lightsSession, iArr);\n            }\n        }\n    };\n\n    public void setFrameColorsInner(LightsManager.LightsSession lightsSession, int[] iArr) { ... }\n}\n```\n\nSee how we have to get through all these if statements in `openSession`? Let's break that down: Essentially, we have a call to `mAtuh.checkAlreadyAuth(ourPackageName)` that has to return true for us to get anywhere. Also, the glyph composer and system apps (pid 1000) explicitly get some special treatment that allows them to run in the background. Similar rules apply in `setFrameColors` — we have to be authenticated and running in the foreground (unless we have special privileges).\n\nThe way we authenticate ourselves seems to be the `register(str)` method, but let's look into the `AuthController.java` file to see how exactly:\n\n```java\npublic boolean register(String packageName, String apikey, int pid) {\n    if (packageName == null || apikey == null || \"\".equals(apikey)) {\n        if (Def.DBG) {\n            Log.d(\"AuthController\", \"pkg:\" + packageName + \", uid:\" + pid);\n        }\n        return false;\n    } else if (pid == 1000) {\n        if (Def.DBG) {\n            Log.d(\"AuthController\", \"register(), system uid\");\n        }\n        addAlreadyAuth(packageName, pid, 2);\n        return true;\n    } else {\n        AuthApp authApp = this.mAuthMap.get(packageName);\n        if (authApp == null) {\n            if (Def.DBG) {\n                Log.d(\"AuthController\", \"Wrong pkg\");\n            }\n            return false;\n        }\n        authApp.setUid(pid);\n        return authApp.checkAuth(apikey, Utils.getCertificateFingerprint(this.mContext, packageName));\n    }\n}\n```\n\nSo system apps (pid 1000) are always accepted, while other apps have to supply an API key. Their package name also has to be present in the `mAuthMap` map, which is provided in JSON format over-the-air by nothing (details in `RemoteConfigController.java`). The `authApp.checkAuth` method just checks that both the api key and the \"sign key\", a SHA1 hash of the calling app's signing key match the expected values provided in the JSON auth map. Here's `Utils.getCertificateFingerprint`:\n\n```java\npublic static String getCertificateFingerprint(Context context, String str) {\n    String str2 = \"\";\n    try {\n        byte[] byteArray = context.getPackageManager().getPackageInfo(str, PackageManager.GET_SIGNATURES).signatures[0].toByteArray();\n        MessageDigest messageDigest = MessageDigest.getInstance(\"SHA1\");\n        messageDigest.update(byteArray);\n        for (byte b : messageDigest.digest()) {\n            String num = Integer.toString(b \u0026 255, 16);\n            if (num.length() == 1) {\n                str2 = str2 + \"0\";\n            }\n            str2 = str2 + num;\n        }\n        return str2.toUpperCase();\n    } catch (PackageManager.NameNotFoundException e) {\n        Log.e(\"ThirdParty:Utils\", e.getMessage(), e);\n        return str2;\n    } catch (NoSuchAlgorithmException e2) {\n        Log.e(\"ThirdParty:Utils\", e2.getMessage(), e2);\n        return str2;\n    }\n}\n```\n\nSo things are looking rather grim — short of begging nothing to give you an API key specifically tied to your app signing credentials, there's not really a way to make this work. And you'll need an extra special key if you want to do things in the background, which is probably even more unrealistic to obtain. You can't extract or steal credentials from other apps, and you can't even patch existing apps that have valid credentials, because either of those options break the signature fingerprint.\n\n# Praxis?\n\nSo what _can_ you do?\n\nWell, there's one interesting quirk: Once an app has registered, it never loses that authentication status, at least from what I can tell. With no real understanding of android service lifecycles I'm really just conjecturing out of my league here, but I think it _might_ be possible to \n\n1. install an authenticated app (i.e. the glyph composer)\n2. let that app register using its credentials\n3. uninstall the app\n4. install your own app, using an identical package name\n5. open a session without registering\n\nIn this case, the `checkAlreadyAuth` check should succeed because it only uses your package name to look up authentication status. Note that I tried this approach briefly and couldn't get it to work, but that might just be my lacking android skills.\n\nBut there's one other and potentially even more powerful trick: Recall the curious line in `openSession` that allows the composer to authenticate without ever calling `register` with an API key:\n\n```java\nif (\"com.nothing.glyph.composer\".equals(callingPackageName) \u0026\u0026 Utils.checkFingerprint(GlyphService.this.mContext, callingPackageName)) {\n    GlyphService.this.mAuth.addAlreadyAuth(callingPackageName, callingUid);\n    ...\n}\n```\n\nIt turns out that bizarrely, the implementation of `checkFingerprint` looks like this:\n\n```java\npublic static boolean checkFingerprint(Context context, String str) {\n        return getCertificateFingerprint(context, str).contains(\"95E1F157FE98518\");\n    }\n```\n\nSee the issue? Not only does it only check for 60 of the 160 bits in the SHA1 hash of the signing key, it also accepts any fingerprint that has these 60 bits **in any position** (aligned to 4bit). This means that it is _very theoretically_ feasible to brute force an android signing key whose SHA1 hash contains this magic substring, which would allow you to impersonate the glyph composer and use the lights as you please.\n\nMy combinatorics are a bit rusty but if my math is correct about 1 in 2^55 keys should have this magic property — that's very very rare, but not completely out there, given that people have been brute-forcing 56bit [DES](https://en.wikipedia.org/wiki/Data_Encryption_Standard) keys successfully many years back, and high-end GPUs seem to be capable of doing so in a handful of days.\n\nIs that worth it for a few blinky lights? I don't know. I guess here's to hoping that nothing will open up the API eventually.\n\n# Notes\n\nIt appears that while the online-config API key distribution thing is fully in place, the glyph composer does not use it at all and is rather patched in to receive similar treatment to a system app - it never calls `register` at all. Curiously, this means that if you were to steal the composer package name, you'd probably lose background privileges because the composer does not technically have the correct permission scope for that and your forged packet would fail the fingerprint check in `openSession`. Now that I think of it, the composer probably doesn't really have these privileges in the first place, since it would fail the foreground check in `setFrameColors`.\n\nOh also: Maybe it's possible to intercept network requests to nothing's servers somehow and inject your own keys into the JSON? I haven't really looked into this since packet captures on non-rooted phones tend to be a bit of a pain. Seems likely that they would use certificate pinning anyway.\n\n# Sample code for when nothing is cool or you spent big money on a brute force idk\n\n```kotlin\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.content.ServiceConnection\nimport android.os.Bundle\nimport android.os.IBinder\nimport android.os.IInterface\nimport android.os.Parcel\nimport android.util.Log\nimport androidx.appcompat.app.AppCompatActivity\n\nclass MainActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        val intent = Intent()\n        intent.setPackage(\"com.nothing.thirdparty\")\n        intent.action = \"com.nothing.thirdparty.bind_glyphservice\"\n        intent.component = ComponentName(\"com.nothing.thirdparty\", \"com.nothing.thirdparty.GlyphService\")\n        applicationContext.bindService(intent, Connection, BIND_AUTO_CREATE)\n\n        Connection.openSession()\n        Connection.setFrameColors(IntArray(33){ it * 10})\n        Connection.closeSession()\n    }\n}\n\nobject Connection : ServiceConnection {\n    private lateinit var glyphI: GlyphI\n    override fun onServiceConnected(className: ComponentName, service: IBinder) {\n        Log.d(\"GlyphManager\", \"Service connected\")\n        glyphI = GlyphI(service)\n    }\n\n    override fun onServiceDisconnected(className: ComponentName) {\n        Log.d(\"GlyphManager\", \"Service disconnected\")\n    }\n\n    fun setFrameColors(values: IntArray) = glyphI.setFrameColors(values)\n    fun openSession() = glyphI.openSession()\n    fun closeSession() = glyphI.closeSession()\n}\n\ninterface GlyphInterface : IInterface {\n    fun setFrameColors(iArr: IntArray)\n    fun closeSession()\n    fun openSession()\n    fun register(str: String): Boolean\n}\n\nclass GlyphI(private val f6990a: IBinder) : GlyphInterface {\n\n    override fun setFrameColors(iArr: IntArray) {\n        val obtain = Parcel.obtain()\n        val obtain2 = Parcel.obtain()\n        try {\n            obtain.writeInterfaceToken(\"com.nothing.thirdparty.IGlyphService\");\n            obtain.writeIntArray(iArr)\n            this.f6990a.transact(1, obtain, obtain2, 0)\n            obtain2.readException()\n        } finally {\n            obtain2.recycle()\n            obtain.recycle()\n        }\n    }\n\n    override fun asBinder(): IBinder {\n        return this.f6990a;\n    }\n\n    override fun closeSession() {\n        val obtain = Parcel.obtain()\n        val obtain2 = Parcel.obtain()\n        try {\n            obtain.writeInterfaceToken(\"com.nothing.thirdparty.IGlyphService\")\n            this.f6990a.transact(3, obtain, obtain2, 0)\n            obtain2.readException()\n        } finally {\n            obtain2.recycle()\n            obtain.recycle()\n        }\n    }\n\n    override fun openSession() {\n        val obtain = Parcel.obtain()\n        val obtain2 = Parcel.obtain()\n        try {\n            obtain.writeInterfaceToken(\"com.nothing.thirdparty.IGlyphService\")\n            this.f6990a.transact(2, obtain, obtain2, 0)\n            obtain2.readException()\n        } finally {\n            obtain2.recycle()\n            obtain.recycle()\n        }\n    }\n\n    override fun register(str: String): Boolean {\n        val obtain = Parcel.obtain()\n        val obtain2 = Parcel.obtain()\n        try {\n            obtain.writeInterfaceToken(\"com.nothing.thirdparty.IGlyphService\");\n            obtain.writeString(str)\n            this.f6990a.transact(4, obtain, obtain2, 0)\n            val res = obtain2.readBoolean()\n            obtain2.readException()\n            return res\n        } finally {\n            obtain2.recycle()\n            obtain.recycle()\n        }\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frec0de%2Fglyph-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frec0de%2Fglyph-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frec0de%2Fglyph-api/lists"}