Security

    Insecure Random Numbers in Mobile Apps

    A sequence of session tokens generated from a time-seeded general purpose random generator, showing a predictable pattern across values.

    Random numbers are the quietest failure in mobile security. Nothing crashes, nothing looks wrong, tests pass, and the tokens your app generates are predictable to anyone who works out which generator produced them. It survives code review because the line looks unremarkable.

    Short answer

    A general-purpose random number generator is not a cryptographic one, and the difference matters wherever the value must be unguessable. Android documents SecureRandom as the cryptographically strong generator, and Apple provides SecRandomCopyBytes for the same purpose. CWE-338 describes the weakness precisely: using a cryptographically weak pseudo-random number generator in a security context. If a value protects something, it comes from the cryptographic generator.

    Where predictable values do damage

    Session and reset tokens. A password reset token generated from a general-purpose generator seeded with the current time is guessable by anyone who knows roughly when it was created, and that is enough for account takeover without touching the device.

    Initialization vectors and nonces. Cryptographic modes have requirements about uniqueness and unpredictability, and a repeated or predictable value can undermine the encryption entirely, sometimes revealing plaintext relationships across messages.

    Salts. A predictable salt defeats the purpose of salting, which is to make precomputation useless.

    Identifiers used for authorization. Any identifier that is treated as a capability, such as a share link or an invitation code, has to be unguessable, and sequential or time-derived identifiers are enumerable.

    Anything used as a one-time code. A verification code from a weak generator is a weak verification code regardless of how many digits it has.

    The pattern worth internalising is that the question is never whether the numbers look random. It is whether an attacker who knows the algorithm and roughly when the value was produced can narrow the search space to something they can try.

    The mistakes that produce weak values

    Using the language's default generator. Every platform ships one, it is faster, it is what appears in tutorials, and it is not designed to resist prediction. This is the overwhelming majority of real cases.

    Seeding a cryptographic generator manually. Supplying your own seed to a secure generator can replace a properly gathered entropy source with something worse, and it is almost never necessary because the platform seeds it correctly.

    Deriving values from time. Timestamps, or a hash of a timestamp, feel unique and are not unpredictable. Uniqueness and unpredictability are different properties and only one of them is a security property.

    Deriving values from device identifiers. Anything stable per device is stable to an attacker who has the device or can observe one value derived from it.

    Truncating properly generated output too far. A cryptographically generated value cut to a handful of characters is small enough to brute force regardless of how it was produced.

    Rolling your own. Combining several weak sources does not produce a strong one, and the composition arguments people use to justify this are generally wrong.

    Choosing sizes that hold up

    Length is the second half of the question, and it is easier to reason about than the generator choice.

    For a value that must resist offline guessing, such as a session token or a reset token, a length on the order of 128 bits of entropy is the usual baseline. Expressed as an encoded string, that is roughly twenty-two characters of base64 or thirty-two hex characters.

    For a value that is rate limited and short-lived, such as a six-digit verification code, the length is deliberately small and the security comes from the rate limit and the expiry rather than from the entropy. That trade is fine as long as both controls actually exist, and they frequently do not.

    For identifiers that are not secrets, such as a database key, unpredictability may not be required at all, and using a cryptographic generator anyway costs nothing worth measuring.

    The reasoning to write down is which of those three categories a given value belongs to, because most disagreements about token length are actually disagreements about that classification.

    Verifying it rather than assuming it

    This is a weakness you find by looking, since it produces no symptoms.

    Search the codebase for the general-purpose generator by name and review every use. The review question is simple: does anything about security depend on this value being unguessable. Most uses are legitimate, such as jitter in a retry backoff or a shuffle in a UI, and the ones that are not stand out quickly.

    Check third-party code with the same question. A library generating identifiers for you has made this decision on your behalf, and the answer is not always the one you would have chosen.

    Look at your server as well as the client. Tokens generated server-side are the ones that matter most, and mobile teams sometimes assume the backend got it right without checking.

    The MASVS cryptography category covers the requirement, and the MASTG documents how to verify it in a built application. An artifact-level scan such as PTKD.com helps with the mechanical part, flagging weak generator usage and related cryptographic configuration in the shipped build, which is where the reasoning about intent then has to start.

    Fixing it without breaking existing users

    Switching generators is a small code change with a migration question attached.

    Values already issued do not become strong retroactively. If reset tokens were weak, the ones outstanding are still weak, and expiring them is part of the fix rather than an optional extra.

    Long-lived credentials generated weakly need rotation. A refresh token issued from a predictable generator two years ago is a live problem, and the remediation is to invalidate and reissue rather than to change the generator going forward and hope.

    Where rotation is disruptive, stage it. Invalidate on next use, force reissue at the next authentication, and set an end date after which old values stop being accepted at all.

    And record which categories of value were affected, because the answer to "did this need fixing" is different for a UI shuffle than for a session token, and someone will ask.

    The generator is only half of it

    A correctly generated token can still be weak, because generation is one of four properties that have to hold together.

    Transmission. A strong token sent over a connection that permits interception is a strong token an attacker has. Randomness does nothing for transport.

    Storage. A token generated properly and then written to a log file, a backup, or an unprotected preferences file is retrievable without any guessing at all. This is a far more common route to a compromised token than prediction is.

    Lifetime. A value that never expires converts a single exposure into permanent access. Short lifetimes are what limit the damage when one of the other three properties fails.

    Validation. A token the server checks by comparison but not for expiry, revocation or scope is being validated only for shape. This is the property most likely to be assumed rather than verified.

    Randomness matters, and it is the property teams focus on because it feels like the cryptographic one. The other three are where more real incidents start.

    Once you have a strong token, how you compare it matters. A comparison that returns as soon as two values differ takes measurably longer for a value matching more leading characters, which leaks information about the correct value across many attempts.

    The fix is a constant-time comparison, which both platforms provide and which most frameworks use internally for exactly this reason. The situations where teams write their own comparison are usually custom token schemes and signature verification, and those are the places to check.

    This matters less on mobile than on a server, since the interesting comparisons happen server-side, and it is worth knowing about because custom verification code in a mobile client tends to be written by whoever also wrote the server side.

    A short audit you can run today

    Four searches, each a few minutes, covering most of the ground.

    Search for the general-purpose random generator by name across the codebase and classify every hit as security-relevant or not.

    Search for the secure generator being constructed with an explicit seed, which is a strong signal of a well-intentioned mistake.

    Search for token or code generation built from timestamps, formatted dates or device identifiers, which tends to appear in helper functions with innocuous names.

    List the third-party libraries that generate identifiers on your behalf and check what each one uses, since that decision was made by someone who did not know your threat model.

    Write down what you found and what you decided for each, because this is precisely the kind of finding that gets rediscovered a year later by someone who assumes it was never reviewed.

    Where randomness shows up in unexpected places

    Two mobile-specific cases are worth knowing because they are easy to miss in a review.

    Device or install identifiers generated at first launch. These often become de facto authentication material later, because some endpoint starts accepting them as a way to identify a user without a session. A predictable install identifier is then a predictable credential, and the value was generated years earlier by code nobody now owns.

    Cache and file names derived from user content. Naming a downloaded file after a hash of its identifier is fine, and naming it after a counter can make one user's cached content addressable by another process on a device where the directory is readable.

    Neither of these looks like a cryptographic decision at the moment it is written, which is precisely why they survive review.

    A note on the values you do not control

    Some of the randomness your app depends on is generated elsewhere, and it is worth knowing which.

    Tokens issued by your identity provider, nonces produced by a payment SDK, and identifiers assigned by your backend are all outside the client's control. The client's job with those is to store them properly and to avoid weakening them, not to second-guess their construction.

    Where it does become your problem is when the client generates something the server then trusts. A device-generated request identifier used for idempotency, a client-side nonce included in a signature, or a locally created invitation code all cross that line, and each deserves the same treatment as a session token.

    The rule that catches these is to ask, for any value the client produces, whether the server makes a decision based on it. If it does, the client is generating security material whether or not anyone called it that.

    Why this survives review

    The reason weak randomness reaches production so reliably is that it reads as ordinary code.

    A line constructing a general-purpose generator looks like every other line around it. There is no dangerous-looking API call, no obviously sensitive string, and no compiler warning. A reviewer scanning for security problems is looking for hardcoded secrets and missing authorization, and a generator choice does not trip either instinct.

    It also produces output that passes every casual check. The values look varied, they do not repeat in testing, and a distribution test would pass, because a general-purpose generator is genuinely good at being statistically random. The property it lacks, resistance to prediction by someone who knows the algorithm, is invisible from the output.

    So the only reliable defence is a deliberate search rather than a hope that review catches it, which is why the audit above is worth scheduling rather than intending.

    What to take away

    Use the platform cryptographic generator for anything that must be unguessable, and the general-purpose one only for values where prediction has no consequence.

    Do not seed a secure generator yourself, do not derive security values from time or device identifiers, and do not truncate output to a length that can be searched.

    Classify each value as needing unpredictability, needing only uniqueness, or needing neither, and let that classification drive the length.

    Verify by searching for the general-purpose generator and reviewing every use, including in dependencies and on the server.

    And when you fix it, expire the values that were issued weakly, because changing the generator does nothing for credentials already in circulation.

    • #randomness
    • #cryptography
    • #session tokens
    • #cwe-338
    • #masvs
    • #secure coding

    Frequently asked questions

    What makes a random number generator insecure?
    A general-purpose generator is built for speed and statistical distribution rather than for resisting prediction. Given the algorithm and some knowledge of the seed, often the current time, an attacker can reproduce the sequence and narrow the search space to something practical. CWE-338 describes exactly this: using a cryptographically weak pseudo-random generator in a security context, where the values need to be unguessable rather than merely varied.
    Which random generator should mobile apps use?
    SecureRandom on Android and SecRandomCopyBytes on Apple platforms, for any value whose security depends on being unguessable. The general-purpose generator remains fine for things where prediction has no consequence, such as jitter in a retry backoff or shuffling a list for display. The distinction to apply is whether an attacker predicting the value would gain anything.
    Should I seed the secure random generator myself?
    No. The platform seeds it from an appropriate entropy source, and supplying your own seed usually replaces a good source with a worse one, particularly when the seed is derived from time or a device identifier. Manual seeding is one of the more common ways a correct generator choice is undermined, and there is almost no situation in mobile application code that requires it.
    How long should a session token be?
    For a value that must resist offline guessing, around 128 bits of entropy is the usual baseline, which is roughly twenty-two base64 characters or thirty-two hex characters. Short values such as six-digit verification codes are a different case: their security comes from rate limiting and expiry rather than from entropy, which is acceptable only when both of those controls genuinely exist in production.
    What do I do about tokens already issued from a weak generator?
    Expire them. Changing the generator only affects values issued afterwards, so anything outstanding remains as predictable as it was. Long-lived credentials such as refresh tokens need rotation rather than a forward-looking fix, and where that is disruptive you can stage it by invalidating on next use and setting an end date after which the old values stop being accepted.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free