Simple offline activation / licensing method for App Inventor apps (device-based, no internet)

Hi everyone,
I want to implement a very simple offline activation (one-time) system for my App Inventor app. Requirements:

When the user first opens the app, show a temporary activation screen (only once until activation).

The activation screen has a button “Get Android ID” (or “Show device ID”) that the user presses.

The app displays the Device S/N (or unique device string). The user sends that ID to me (the developer).

I generate a serial / activation code on my side (offline) and send it back to the user.

The user enters the serial in the app; if valid, the app is activated and opens normally.

Everything must work offline (no server / no internet) Manually or automatically

Include your postal address on the splash screen, where they can request activation codes.

Have them include a self addressed stamped envelope.

Mail them back activation codes.

Are you sure you want to do this all manually? You could automate the whole thing...

The user sends me their device serial number

and pays for the application using my payment methods

Then I send them the activation serial number

How can I do this automatically offline without a server?

I'm considering using Google Sheets instead for automatic one-time activation.

No, I will provide a WhatsApp contact link to send the device S/N , and after payment confirmation, I will send him the activation serial number.

Are you able to get the device serial number programmatically for newer devices? What did you try? If I remember correctly this is not possible anymore, however there are methods to get a unique number

Taifun

I asked ChatGPT for you
ChatGPT was a bit haluzinating again... there is no such extension TaifunDeviceTools or TaifunCryptography available... but there are other extensions available, just search the extensions directory App Inventor Extensions | Pura Vida Apps

Taifun


Excellent — this is a classic “offline activation” flow, and it can be implemented cleanly in App Inventor (or any derivative such as Kodular, Niotron, etc.) even with targetSdkVersion 35, though you’ll need to design around Android’s new privacy limits.

Let’s break it down in a developer-oriented way:


:gear: Step 1. Choose your “device ID”

Because of Android’s restrictions, you cannot reliably read the hardware serial (Build.SERIAL) or IMEI anymore (it returns unknown on modern Android versions).
For offline activation, use one of the following:

  • :white_check_mark: Android ID (Settings.Secure.ANDROID_ID) — available without permissions; stable per device/user.
  • Or create your own persistent random UUID stored in TinyDB on first run.

:point_right: For simplicity:
Use the Device Tools extension (e.g. TaifunDeviceTools) → AndroidId block.


:gear: Step 2. Show it to the user

The activation screen has:

  • Label showing “Your Device ID”
  • Button “Copy ID” or “Send to Developer”
  • TextBox for entering activation code
  • “Activate” button

When pressed:

LabelDeviceID.Text ← DeviceTools.AndroidId

:gear: Step 3. Generate the activation code (developer side)

You want a reproducible function that:

  1. Takes the DeviceID as input
  2. Generates a short activation code (that can be validated offline inside the app)

You can do this via a simple hash-based scheme:

:1234: Option A: Hash + Secret key

Use a small program (in Python, Java, etc.) with a private secret key known only to you:

import hashlib

def make_activation(device_id):
    secret = "MyPrivateSecretKey2025"
    data = (device_id + secret).encode()
    code = hashlib.sha256(data).hexdigest().upper()
    # Shorten for convenience:
    return code[:10]  # e.g., first 10 chars

So if a user sends ANDROID_ID = a1b2c3d4e5f6g7h8,
you generate and send back DF83A72C5E.


:gear: Step 4. Validate in the App

Inside App Inventor, you replicate the same logic (using the same secret key),
then compare the computed code with what the user entered.

App Inventor has a SHA256 block in the Cryptography extension
(e.g. TaifunCryptography):

expected ← SHA256(DeviceID + "MyPrivateSecretKey2025")
expected ← UPPERCASE expected
expected ← segment text expected from 1 to 10

Then:

if enteredCode = expected
   then set TinyDB tag "Activated" to true
        open main screen
else
   show message "Invalid code"

:gear: Step 5. Remember activation

On app start:

if TinyDB.GetValue("Activated") = true
   then open main screen
else
   open ActivationScreen

That’s all.


:white_check_mark: Summary of the activation logic

Step User Side (App) Developer Side
1 Show AndroidId Receives it
2 Run SHA256(AndroidId + secret)
3 User enters activation code App repeats same formula
4 If match → save flag

:bulb: Security Notes

  • Keep your secret key private — if someone extracts the .aia they could read it.
    To make reverse-engineering harder, obfuscate it (e.g. split into parts, encode).
  • Android ID may reset after factory reset or on new user profile, so user may need reactivation in that case.
1 Like

rotection.aia (378.1 KB)



blocks(49)

The problem has already been solved this way, but I'm afraid to use DEVICE ID because it might require permission. I want to use ANDROID ID

HOW CAN I GET ANDROID ID WITHOUT PERMISSIONS

Search the extensions directory App Inventor Extensions | Pura Vida Apps for device extensions or write your own extension

package com.yourcompany.yourextension; // Replace with your package structure

import android.provider.Settings;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.ComponentContext;

// ... other necessary imports

public class YourExtensionName extends AndroidNonvisibleComponent {

    public YourExtensionName(ComponentContext context) {
        super(context);
    }

    /**
     * Returns the unique ANDROID_ID for the device.
     */
    @SimpleFunction(description = "Returns the unique ANDROID_ID for the device.")
    public String GetAndroidId() {
        return Settings.Secure.getString(
            this.container.$context().getContentResolver(),
            Settings.Secure.ANDROID_ID
        );
    }
}

Taifun

blocks(52)



PYTHON CODE GENERATOR

import hmac
import hashlib
import base64

SECRET_KEY = "APP INVENTOR"

def generate_activation_code_base64(android_id: str, length: int = 12) -> str:
    """
    Generate activation code by HMAC-SHA256 -> base64 -> clean -> take first `length` chars.
    Returns uppercase string.
    Matches App Inventor flow when you remove '=', '+', '/' then take segment(...,1,length).
    """
    android_id = android_id.strip()
    hmac_bytes = hmac.new(SECRET_KEY.encode('utf-8'), android_id.encode('utf-8'), hashlib.sha256).digest()
    b64 = base64.b64encode(hmac_bytes).decode('utf-8').strip()
    clean = b64.replace('=', '').replace('+', '').replace('/', '')
    return clean[:length].upper()

def generate_activation_code_numeric(android_id: str, length: int = 8) -> str:
    """
    Generate numeric-only activation code of given length.
    We convert HMAC bytes to a big integer then take modulo 10**length to get fixed-length digits (leading zeros preserved).
    """
    android_id = android_id.strip()
    hmac_bytes = hmac.new(SECRET_KEY.encode('utf-8'), android_id.encode('utf-8'), hashlib.sha256).digest()
    num = int.from_bytes(hmac_bytes, byteorder='big', signed=False)
    mod = 10 ** length
    code_num = num % mod
    return str(code_num).zfill(length)

def verify_code(user_code: str, android_id: str, method: str = 'base64', length: int = 12) -> bool:
    """Verify user's code against generated code (method: 'base64' or 'numeric')."""
    user_code = user_code.strip().upper()
    if method == 'base64':
        expected = generate_activation_code_base64(android_id, length)
    else:
        expected = generate_activation_code_numeric(android_id, length)
    return user_code == expected

# ---- Example usage ----
if __name__ == "__main__":
    sample_id = input("أدخل Android ID: ").strip()
    # مثال Base64-clean بطول ثابت 12
    print("Base64-clean activation (12 chars):", generate_activation_code_base64(sample_id, 12))
    # مثال رقمي بطول ثابت 8
    print("Numeric activation (8 digits):", generate_activation_code_numeric(sample_id, 8))

    # اختبار تحقق
    test_code = input("للتجربة: ادخل كود للتحقق (أو اضغط Enter لتخطي): ").strip()
    if test_code:
        ok_base64 = verify_code(test_code, sample_id, method='base64', length=12)
        ok_numeric = verify_code(test_code, sample_id, method='numeric', length=8)
        print("مطابقة (base64,12):", ok_base64, "| مطابقة (numeric,8):", ok_numeric)

Protection.aia (384.0 KB)

VBA CODE GENERATOR BY EXCEL SHEETS

' =======================================================
' ✅ HMAC-SHA256 → Base64-clean → First 12 uppercase chars
' Secret Key = "APP INVENTOR"
' =======================================================

Option Explicit

'--- دالة رئيسية لاستعمالها في الخلايا ---
Function HMAC_SHA256_12(AndroidID As String, Optional SecretKey As String = "APP INVENTOR") As String
    On Error GoTo ErrHandler
    
    Dim hash() As Byte
    Dim base64 As String
    Dim clean As String
    
    ' التأكد من وجود Android ID
    If Trim(AndroidID) = "" Then
        HMAC_SHA256_12 = "❌ Missing Android ID"
        Exit Function
    End If
    
    ' حساب HMAC-SHA256 باستخدام الدالة المساعدة
    hash = ComputeHMAC_SHA256(AndroidID, SecretKey)
    
    ' تحويل الناتج إلى Base64
    base64 = EncodeBase64(hash)
    
    ' تنظيف الرموز = + /
    clean = Replace(Replace(Replace(base64, "=", ""), "+", ""), "/", "")
    
    ' أخذ أول 12 حرف وتحويلها لحروف كبيرة
    HMAC_SHA256_12 = UCase(Left(clean, 12))
    Exit Function
    
ErrHandler:
    HMAC_SHA256_12 = "⚠️ Error"
End Function


'--- دالة حساب HMAC-SHA256 باستخدام Windows API ---
Private Function ComputeHMAC_SHA256(ByVal message As String, ByVal key As String) As Byte()
    Dim enc As Object, converter As Object
    Dim keyBytes() As Byte, msgBytes() As Byte
    Dim hmac As Object
    
    Set enc = CreateObject("System.Text.UTF8Encoding")
    Set converter = CreateObject("System.Security.Cryptography.HMACSHA256")
    
    keyBytes = enc.GetBytes_4(key)
    msgBytes = enc.GetBytes_4(message)
    
    converter.Key = keyBytes
    ComputeHMAC_SHA256 = converter.ComputeHash_2(msgBytes)
End Function


'--- دالة تحويل بايتات إلى Base64 ---
Private Function EncodeBase64(bytes() As Byte) As String
    Dim xml As Object
    Set xml = CreateObject("MSXML2.DOMDocument")
    xml.LoadXML "<root />"
    xml.DocumentElement.DataType = "bin.base64"
    xml.DocumentElement.nodeTypedValue = bytes
    EncodeBase64 = xml.DocumentElement.Text
    Set xml = Nothing
End Function

GOOGLE SHEET SCRIPT CODE GENERATOR

/**
 * HMAC-SHA256 -> Base64-clean -> first 12 chars (UPPERCASE)
 * Default secret key is "APP INVENTOR" unless you pass a different key.
 *
 * Usage in sheet:
 * =HMAC_SHA256_12(A1)                     // uses default key "APP INVENTOR"
 * =HMAC_SHA256_12(A1, "ARADY AHMED ...") // override key if needed
 */
function HMAC_SHA256_12(android_id, secret_key) {
  // Use default key "APP INVENTOR" if not provided
  if (typeof secret_key === 'undefined' || secret_key === null || String(secret_key).trim() === "") {
    secret_key = "APP INVENTOR";
  }

  // Ensure inputs are strings
  android_id = String(android_id || "");
  secret_key = String(secret_key || "");

  // If android_id missing -> return helpful message
  if (android_id.trim() === "") {
    return "❌ Missing Android ID";
  }

  try {
    // compute HMAC-SHA256 signature (returns byte[]-like)
    var signature = Utilities.computeHmacSha256Signature(android_id, secret_key);

    // base64 encode the raw bytes
    var b64 = Utilities.base64Encode(signature);

    // remove padding and problematic chars to match "clean" form
    var clean = b64.replace(/=|\+|\//g, '');

    // take first 12 chars and return uppercase
    var code = clean.substring(0, 12).toUpperCase();

    return code;
  } catch (e) {
    return "Error: " + e.message;
  }
}

Please let us know, where you found that extension which returns the android id

Taifun