Device Impersonation: Defense Techniques

Using cryptography as device identification

A device identification must not be simply a block of information that is sent from the device. Any static information is easy to intercept and spoof later.

Instead, the technique is to generate a cryptographic key pair and use the private key (which would identify the device) to sign the payloads that are sent to the server side. This signature will prove that the payload was generated with a legitimate registered device. The server can check with which key the signature was made, and thus identify the device associated with that key. Signature integrity will prove that the device is in possession of the right key, while the challenge value, current date and time, or any other variable indicator will prove that the signature was applied recently.

Code examples:

// Key generation private const val ANDROID_KEYSTORE = "AndroidKeyStore" private const val KEY_ALIAS = "Binding" fun generateEcKeyPair( context: Context ): KeyPair { val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY val builder = KeyGenParameterSpec.Builder(KEY_ALIAS, purposes) .setDigests( KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512 ) builder.setCertificateSubject(X500Principal("CN=$KEY_ALIAS")) builder.setCertificateSerialNumber(java.math.BigInteger.valueOf(System.currentTimeMillis())) builder.setCertificateNotBefore(java.util.Date()) builder.setCertificateNotAfter(java.util.Date(System.currentTimeMillis() + 3650L * 24 * 60 * 60 * 1000)) val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE) kpg.initialize(builder.build()) return kpg.generateKeyPair() }

// Signing of a JSON payload fun sign(data: ByteArray): ByteArray { val ks = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } val privateKey = ks.getKey(KEY_ALIAS, null) as PrivateKey val sig = Signature.getInstance("SHA256withECDSA") sig.initSign(privateKey) sig.update(data) return sig.sign() }

// Checking the signature and identifying the key at the server side // The server needs the device’s public key to verify signatures. // The public key is included in the X.509 certificate. // Share this certificate with the server during the initial handshake. fun getCertificate(): X509Certificate? { val ks = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } return ks.getCertificate(KEY_ALIAS) as? X509Certificate } fun verify(data: ByteArray, signature: ByteArray, cert:X509Certificate): Boolean { val sig = Signature.getInstance("SHA256withECDSA") sig.initVerify(cert.publicKey) sig.update(data) return sig.verify(signature) }

Securely storing the device identifier

The private key that identifies the device can be stolen through either remote or brief physical access. To mitigate this, use the TEE-backed storage for the key.


// By default, Android will store the private key generated by 'generateEcKeyPair' // in the device's Trusted Execution Environment (TEE), if a TEE is available. // // If no TEE exists, the system falls back to a software-backed keystore. // // To check where a key was created after generation: // // - On API 31+ (Android 12+), use KeyInfo.getSecurityLevel() // returns STRONGBOX, TRUSTED_ENVIRONMENT (TEE), or SOFTWARE. // // - On older APIs, use KeyInfo.isInsideSecureHardware() // returns true if the key is hardware-backed (TEE or StrongBox). // Up to API 31 if(privateKey.isInsideSecureHardware()) { /* In TEE */ } // Avaliable form API 31 if(privateKey.getSecurityLevel() == TRUSTED_ENVIRONMENT) { /* In TEE */ } // Some modern devices have an even more resilient version of TEE called StrongBox. // StrongBox uses a dedicated secure chip (separate from the main CPU) that provides // extra resistance against both software and physical attacks. // If StrongBox is available on the device, you should prefer it for key generation. // // To request StrongBox explicitly, use: // builder.setIsStrongBoxBacked(true) // // If StrongBox is not present, Android will throw StrongBoxUnavailableException. // In that case, you can fall back to a regular TEE-backed keystore.

Residual risk: Theft of the physical device

A hacker can steal the device that the victim is operating and keep it for a prolonged time. This time could be sufficient to unlock the device and make use of the protected application.

A number of techniques exist to mitigate the risk. First and foremost, your application must support users reporting stolen devices, which would invalidate their public key at the server side. This would limit the time window when the victim’s digital assets are accessible to the attacker.

Further hardening includes the application disabling user access based on the number of unsuccessful login attempts. An attacker might have the device, but they do not necessarily have the credentials of the victim user, which could provide an extra time window for you and the victim to react.

Guardsquare

Table of contents