Case study · Aegis Authenticator

How does an authenticator
generate a login code?

A six-digit code changes every 30 seconds. Use REA to find the calculation in an Android APK, then rebuild it with a clock you can control.

Aegis 3.4.3 · Android · Official release APK · 6.9 MB

View Aegis on GitHub

From an APK to a code you can reproduce

  1. 01 · Locate the feature3,077 classes → generating methodsREA searches the package and traces the code used by the display and copy actions.
  2. 02 · Read the calculationTime + account key → six digitsREA returns the methods and defaults. The agent explains how they fit together.
  3. 03 · Rebuild and checkChange the clock. Watch the code.A new browser demo reproduces the rule and checks it against public reference cases.

Ask your agent

The app shows a changing login code. We want to find what makes it change and reproduce that behavior.

Your coding agent

Use REA to inspect aegis-v3.4.3.apk. Find how it generates the six-digit login code, explain why it changes every 30 seconds, and rebuild a small demo where I can change the time.

An example prompt for the downloaded APK, with REA's Android analysis configured. Get the APK and analysis setup ↓

Try the code generator

The app groups time into 30-second blocks. It combines the block number with the account key to generate a code. The same key and block produce the same code.

Move the clock across 60 seconds

  1. Time59 secondsOur adjustable example clock
  2. 30-second blockBlock 1floor(59 / 30)
    30–59 seconds
  3. Generated code287082Account key + block → six digits

At 59 seconds: 287082. At 60 seconds: 359152. Moving within one block leaves the code unchanged.

The key is fixed, public RFC 6238 test data. The demo implements the calculation recovered from the APK.

Find the code with REA

  1. Narrow the APK to three classes

    Agent → REA
    inspect_android_package
    {"path": "aegis-v3.4.3.apk"}
    
    search_android_classes
    {"path": "aegis-v3.4.3.apk",
     "query": "Totp"}
    REA → agent · selected results
    Package: com.beemdevelopment.aegis
    Version: 3.4.3
    Classes: 3,077
    
    Three matches:
      otp.TotpInfo
      ui.views.TotpProgressBar
      importers.TotpAuthenticatorImporter

    TotpInfo is the calculation candidate; the other two concern the progress bar and imported accounts. TOTP means a time-based one-time password.

  2. Connect the generator to the app's display

    Agent → REA
    inspect_android_class
    {"path": "aegis-v3.4.3.apk",
     "class_name":
      "com.beemdevelopment.aegis.otp.TotpInfo"}
    
    trace_android_references
    {"path": "aegis-v3.4.3.apk",
     "class_name":
      "com.beemdevelopment.aegis.otp.OtpInfo",
     "method_name": "getOtp"}
    REA → agent · methods and callers
    TotpInfo methods:
      getOtp()      · overload 0
      getOtp(long)  · overload 1
    
    OtpInfo.getOtp callers:
      ui.views.EntryHolder.getOtp
      ui.MainActivity.copyEntryCode

    REA identifies the timestamp overload and finds the base method's callers. Inspecting EntryHolder.getOtp then shows the display calling TotpInfo.getOtp(timestamp).

Requests show a shortened input path. Result class names omit the common com.beemdevelopment.aegis prefix.

Read the calculation

REA returns the clock conversion, default settings and generating methods. The constructors set a 30-second interval and six digits; the timestamp method turns seconds into a block number.

REA · selected decompiled Java
// TotpInfo defaults and clock
setPeriod(30);
getOtp(System.currentTimeMillis() / 1000);

// OtpInfo default algorithm and digits
this(bArr, "SHA1", 6);

// Block passed to the generator
(long) Math.floor(j / ((double) this._period))

// generateOTP → getHash
byte[] hash = getHash(bArr, str, j);

// getHash: account key + encoded block
Mac mac = Mac.getInstance(str);
mac.init(secretKeySpec);
return mac.doFinal(bArrArray);

// OTP.toString, case 0
this._code % ((int) Math.pow(10.0d, i))

// Keep leading zeroes
while (sb.length() < i) {
    sb.insert(0, "0");
}
Readable summary · agent's interpretation
// Group time into 30-second blocks
seconds = current_time_ms / 1000
block = floor(seconds / 30)

// Generate a number from the key and block
hash = HMAC_SHA1(account_key,
                 big_endian_8_bytes(block))
number = select_31_bits(hash)

// Format the default six-digit code
code = number % 1_000_000
code = pad_with_zeroes(code, 6)

Time chooses the block. Seconds 30 through 59 all give block 1. At 60 seconds, the block becomes 2.

The key matters too. The generator combines the account key with the block. An account with a different key can show a different code at the same time.

Keep six digits. The remainder after division by 1,000,000 is padded with zeroes when needed, so a code can begin with 0.

Excerpts from several inspected methods, with line wrapping for display. The right panel is an explanatory summary. Aegis code is GPL-3.0.

Inside the generator

The released APK puts generateOTP in kotlin.ExceptionsKt. REA follows the call from TotpInfo and returns this body; the original source calls the helper HOTP.

REA · generateOTP, complete body
public static OTP generateOTP(byte[] bArr, String str,
        int i, long j)
        throws NoSuchAlgorithmException, InvalidKeyException {
    byte[] hash = getHash(bArr, str, j);
    int i2 = hash[hash.length - 1] & 15;
    return new OTP(
        (hash[i2 + 3] & 255)
        | ((hash[i2] & 127) << 24)
        | ((hash[i2 + 1] & 255) << 16)
        | ((hash[i2 + 2] & 255) << 8), i, 0);
}

The last hash byte selects a position. Four bytes at that position provide a positive 31-bit number; formatting reduces it to the requested number of digits. The constructor's last argument selects the code-formatting branch introduced by compilation.

Compare with the matching release source.

Check the reconstruction

We assembled the selected REA-returned Java methods in a small test program. Their output matched an independent reconstruction on 133 test cases, including time-block boundaries and codes beginning with zero.

Example time Block Six-digit code
30 seconds 1 287082
59 seconds 1 287082
60 seconds 2 359152
1,111,111,109 seconds 37,037,036 081804

The generator also matches the six published SHA-1 reference cases in RFC 6238. Check the browser implementation against them:

Read the demo's calculation

The reference cases use eight digits. The check also verifies their six-digit results.

Verification runs extracted Java methods and the new demo. The Android APK was inspected statically.

Inspect the APK yourself

Set up REA on macOS or Linux, then download these two files into the same folder. On Windows, use a Linux environment such as WSL.

Android analysis needs a full JDK 17 or later. Select it with JAVA_HOME or PATH. This case used JDK 21.

Point REA at the provider file and inspect the APK:

macOS / Linux
REA_JADX_MCP_JAR="$PWD/jadx-headless-mcp-0.7.1-all.jar" \
npx rea-agents@latest inspect-android-package \
  ./aegis-v3.4.3.apk --format json --full-output

Your agent can continue through REA's CLI using the prompt above. For MCP, add REA_JADX_MCP_JAR to the REA server's environment and reconnect it.

A next question: what happens when an account uses eight digits or a different time interval?

Sources

Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited to Aegis; the diagram, readable summary and browser reconstruction are new teaching material.

Top