Android nfc эмуляция карты с фиксированным uid
– coderoad
Одно важно знать, что UID перенесли на очень низком уровне протокола nfc. Это значит, что это делается самостоятельно прошивкой nfc а не в рамках операционной системы android. У нас была такая же проблема в нашем NFCGate проекте и мы нашли решение для чипов Broadcom BCM20793 наподобие тех, что есть в Nexus4/5 и прочих, записав UID с NFC_SetConfig прямо в прошивку чипа.
Вы можете посмотреть рабочую версию в нашем репозитории на github . Вот не тестированная версия, чтобы показать принцип:
uint8_t cfg[] = {
CFG_TYPE_UID, // config type
3, // uid length
0x0A, // uid byte 1
0x0B, // uid byte 2
0x0C // uid byte 3
};
NFC_SetConfig(sizeof(cfg), cfg);
Наши тесты выявили, что android иногда устанавливает UID обратно в рандомный (length=0 если я правильно помню), поэтому вам нужно найти хорошее место, чтобы задать его, когда вам это нужно или сделать что-то похожее, как мы сделали и перехватить вызовы NFC_SetConfig из android, чтобы заново установить наш собственный UID.
Can i change the uid of nfc tags
No, the UID of genuine Type 1 tags (from Broadcom or, formerly, Innovision) cannot be changed. That UID is a serial number that is permanently burned-in into read-only memory during the manufacturing process.
I’m not sure for Type 1 tags, but there are certainly other NFC-A tags available (typically from Chinese suppliers) that behave similar to NXP MIFARE products and permit changing the UID using special commands.
Note that it’s a really bad idea to use the UID/anti-collision identifier (or any other freely readable data) of NFC/RFID tags for authentication purposes since that information can easily be cloned. See also:
Keychain — связка ключей
Представленный в Android 4.0 (API Level 14), Keychain API управлял ключами. В частности, это работает с объектами PrivateKey и X509Certificate и обеспечивает более безопасный контейнер, чем использование хранилища данных вашего приложения.
Связано это с тем, что разрешения закрытых ключей открывают доступ к ключам только вашему приложению и только после авторизации пользователя. Это означает, что, прежде чем, вы сможете использовать хранилище учётных данных, на устройстве должен быть настроен экран блокировки. Кроме того, объекты в связке ключей можно объединить с защитой от оборудования, если доступно.
Код установки сертификата выглядит следующим образом:
Intent intent = KeyChain.createInstallIntent(); byte[] p12Bytes = //... read from file, such as example.pfx or example.p12... intent.putExtra(KeyChain.EXTRA_PKCS12, p12Bytes); startActivity(intent);
Пользователю будет предложено ввести пароль для доступа к закрытому ключу и указать имя сертификата. Для получения ключа, в следующем коде представлен пользовательский интерфейс, который позволяет пользователю выбирать ключ из списка установленных ключей.
Keystore
В предыдущем уроке, мы рассмотрели защиту данных с помощью предоставляемого пользователем пароля. Такой вариант хорош, но требования к приложениям часто уводят от того, чтобы пользователи каждый раз входили в систему и запоминали дополнительный пароль.
Вот где можно использовать KeyStore API. Начиная с API 1, KeyStore используется системой для хранения учётных данных WiFi и VPN. Начиная с 4.3 (API 18), вы можете работать с асимметричными ключами конкретного приложения, а в Android M (API 23) можно хранить симметричный ключ AES.
Преимущество хранения ключа в хранилище ключей заключается в том, что он позволяет работать с ключами без раскрытия секретного содержимого этого ключа; данным ключа не место в приложении. Помните, что ключи защищаются разрешениями, так что только ваше приложение может получить к ним доступ, и они могут быть дополнительно защищены аппаратным обеспечением, если устройство поддерживает это. Создаётся контейнер, который усложняет извлечение ключей с устройства.
Генерирование нового случайного ключа
В этом примере вместо генерации ключа AES из предоставленного пользователем пароля мы можем автоматически сгенерировать случайный ключ, который будет защищён в хранилище ключей KeyStore. Мы можем сделать это, создав экземпляр KeyGenerator, настроенного на поставщика “AndroidKeyStore”.
Диспетчер учётных записей
Диспетчер учётных записей (Account Manager) — это централизованный помощник для работы с учётными данными пользователя, поэтому вашему приложению, иметь дело с паролями напрямую не нужно. Часто он предоставляет токен вместо реального имени пользователя и пароля, который можно использовать для выполнения аутентифицированных запросов к службе. Например, при запросе токена OAuth2.
Иногда, вся необходимая информация уже хранится на устройстве, а иногда Account Manager придётся обращаться к серверу за новым токеном. Вы, наверное, видели раздел Учётные записи в Настройках вашего устройства для разных приложений. И вот как, мы можем получить список доступных учётных записей:
AccountManager accountManager = AccountManager.get(this); Account[] accounts = accountManager.getAccounts();
Этому коду потребуется разрешение android.permission.GET_ACCOUNTS. Если вы ищете определённую учётную запись, вы можете найти её вот так:
AccountManager accountManager = AccountManager.get(this); Account[] accounts = accountManager.getAccountsByType("com.google");
Использование асимметричных ключей rsa для старых устройств
Это хорошее решение для хранения данных в версии M и выше, но что, если ваше приложение поддерживает более ранние версии? Хотя симметричные ключи AES не поддерживаются в M, поддерживаются асимметричные ключи RSA. Это означает, что для достижения того же результата, мы можем использовать RSA ключи и шифрование.
Основное отличие заключается в том, что асимметричная пара ключей содержит два ключа: закрытый и открытый ключ, где открытый ключ шифрует данные, а закрытый ключ расшифровывает их. KeyPairGeneratorSpec передаётся в KeyPairGenerator, который инициализируется с помощью KEY_ALGORITHM_RSAи поставщика AndroidKeyStore.
private void testPreMEncryption() { try { //Generate a keypair and store it in the KeyStore KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this) .setAlias("MyKeyAlias") .setSubject(new X500Principal("CN=MyKeyName, O=Android Authority")) .setSerialNumber(new BigInteger(1024, new Random())) .setStartDate(start.getTime()) .setEndDate(end.getTime()) .setEncryptionRequired() //on API level 18, encrypted at rest, requires lock screen to be set up, changing lock screen removes key .build(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize(spec); keyPairGenerator.generateKeyPair(); //Encryption test final byte[] encryptedBytes = rsaEncrypt("My secret string!".getBytes("UTF-8")); final byte[] decryptedBytes = rsaDecrypt(encryptedBytes); final String decryptedString = new String(decryptedBytes, "UTF-8"); Log.e("MyApp", "Decrypted string is " decryptedString); } catch (Throwable e) { e.printStackTrace(); } }
Для шифрования, из пары ключей мы получаем RSAPublicKey и используем его с объектом Cipher.
public byte[] rsaEncrypt(final byte[] decryptedBytes) { byte[] encryptedBytes = null; try { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); final RSAPublicKey publicKey = (RSAPublicKey)privateKeyEntry.getCertificate().getPublicKey(); final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); cipherOutputStream.write(decryptedBytes); cipherOutputStream.close(); encryptedBytes = outputStream.toByteArray(); } catch (Throwable e) { e.printStackTrace(); } return encryptedBytes; }
Расшифровка выполняется с использованием объекта RSAPrivateKey.
public byte[] rsaDecrypt(final byte[] encryptedBytes) { byte[] decryptedBytes = null; try { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); final RSAPrivateKey privateKey = (RSAPrivateKey)privateKeyEntry.getPrivateKey(); final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); cipher.init(Cipher.DECRYPT_MODE, privateKey); final CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encryptedBytes), cipher); final ArrayList<Byte> arrayList = new ArrayList<>(); int nextByte; while ( (nextByte = cipherInputStream.read()) != -1 ) { arrayList.add((byte)nextByte); } decryptedBytes = new byte[arrayList.size()]; for(int i = 0; i < decryptedBytes.length; i ) { decryptedBytes[i] = arrayList.get(i); } } catch (Throwable e) { e.printStackTrace(); } return decryptedBytes; }
Кое-что об RSA — шифрование медленнее, чем в AES. Для небольших объёмов информации, например, когда вы защищаете строки общих настроек, это не страшно. Если вы обнаружите проблему с производительностью при шифровании больших объёмов данных, то вместо этого вы можете использовать данный пример для шифрования и хранения только ключа AES.
И тогда, для остальной части ваших данных, используйте более быстрое шифрование AES, которое обсуждалось в предыдущем уроке. Вы можете сгенерировать новый AES ключ и преобразовать его в массив byte[], который совместим с этим примером.
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(256); //AES-256 SecretKey secretKey = keyGenerator.generateKey(); byte[] keyBytes = secretKey.getEncoded();
Чтобы получить ключ, сделайте вот так:
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
Довольно много кода! Для простоты примеров, я пропустил обработку исключений. Но помните, что для итогового кода не рекомендуется просто перехватывать все случаи Throwable в одном операторе catch.
Расшифровка в массив байтов
Для расшифровки применяется обратный ход. Объект Cipher инициализируется с использованием константы DECRYPT_MODE, и возвращается расшифрованный массив byte[].
private byte[] decrypt(final HashMap<String, byte[]> map) { byte[] decryptedBytes = null; try { //Get the key final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey(); //Extract info from map final byte[] encryptedBytes = map.get("encrypted"); final byte[] ivBytes = map.get("iv"); //Decrypt data final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); final GCMParameterSpec spec = new GCMParameterSpec(128, ivBytes); cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal(encryptedBytes); } catch (Throwable e) { e.printStackTrace(); } return decryptedBytes; }
Смотрим на примере
Теперь мы можем посмотреть на пример!
Шифрование данных
Теперь, когда ключ хранится в хранилище KeyStore, мы можем создать метод, который зашифрует данные с использованием объекта Cipher, учитывая SecretKey. Это вернёт HashMap, содержащий зашифрованные данные и случайный ВИ, который понадобится для расшифровки данных. Зашифрованные данные вместе с ВИ могут быть сохранены в файл или в открытых настройках.
private HashMap<String, byte[]> encrypt(final byte[] decryptedBytes) { final HashMap<String, byte[]> map = new HashMap<String, byte[]>(); try { //Get the key final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey(); //Encrypt data final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); final byte[] ivBytes = cipher.getIV(); final byte[] encryptedBytes = cipher.doFinal(decryptedBytes); map.put("iv", ivBytes); map.put("encrypted", encryptedBytes); } catch (Throwable e) { e.printStackTrace(); } return map; }
Заключение
На этом урок по работе с учётными данными и ключами завершён. Большая часть неразберихи вокруг ключей и хранилища связана с эволюцией ОС Android, но вы можете выбрать, какое решение использовать, учитывая уровень API, поддерживаемый вашим приложением.
Теперь, когда мы рассмотрели лучшие примеры защиты данных в состоянии покоя, следующий урок будет сосредоточен на защите данных при передаче.