Android
アンドロイドの指紋認証API
サーチ…
備考
Androidアプリケーションでの指紋スキャナの追加
AndroidはAndroid 6.0(Marshmallow)SDK 23の指紋APIをサポートしています
アプリでこの機能を使用するには、まずマニフェストにUSE_FINGERPRINT権限を追加します。
<uses-permission
android:name="android.permission.USE_FINGERPRINT" />
ここに従う手順
まず、ユーザーが指紋で認証してKeyGenParameterSpecを渡した後にのみ使用できるKeyGeneratorを使用して、Android Key Storeに対称キーを作成する必要があります。
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
keyPairGenerator.initialize(
new KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
.setUserAuthenticationRequired(true)
.build());
keyPairGenerator.generateKeyPair();
KeyGenParameterSpec.Builder.setUserAuthenticationRequiredをtrueに設定すると、ユーザーの指紋で認証された場合を含め、ユーザーが認証した後でのみキーの使用を許可できます。
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PublicKey publicKey =
keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey();
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PrivateKey key = (PrivateKey) keyStore.getKey(KEY_NAME, null);
その後、FingerprintManager.authenticateを呼び出し、指紋センサーの指紋を聞き始めると、作成された共通鍵で初期化された暗号が作成されます。あるいは、認証者としてサーバー側で確認されたパスワードに戻すこともできます。
fingerprintManger.class
からFingerprintManger
作成して初期化する
getContext().getSystemService(FingerprintManager.class)
認証するには、 FingerprintManger
APIを使用し、
FingerprintManager.AuthenticationCallback
メソッドをオーバーライドします。
onAuthenticationError
onAuthenticationHelp
onAuthenticationSucceeded
onAuthenticationFailed
始めること
fingerPrintイベント・コールを開始するには、暗号化メソッドを使用します。
fingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 , this, null);
キャンセル
スキャナコールのリスンを停止する
android.os.CancellationSignal;
指紋(またはパスワード)が検証されると、FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded()コールバックが呼び出されます。
@Override
public void onAuthenticationSucceeded(AuthenticationResult result) {
}
Android Fingerprint APIを使用してユーザーのパスワードを保存する方法
このヘルパークラスの例では、指紋プリントマネージャと対話し、パスワードの暗号化と復号化を実行します。この例の暗号化に使用される方法はAESです。これは唯一の暗号化方法ではなく、 他の例も存在します。この例では、データは次のように暗号化され、解読されます。
暗号化:
- ユーザーはヘルパーに暗号化されていないパスワードを与えます。
- ユーザーは指紋を提供する必要があります。
- 認証されると、ヘルパーは
KeyStore
からキーを取得し、Cipher
を使用してパスワードを暗号化します。 - パスワードとIV塩(IVはすべての暗号化で再作成され、再利用されません)は共有プリファレンスに保存され、復号化プロセスの後半で使用されます。
復号化:
- ユーザーのパスワード解読要求。
- ユーザーは指紋を提供する必要があります。
- ヘルパーはIVを使用して
Cipher
を構築し、ユーザーが認証されると、KeyStoreはキーKeyStore
からキーを取得し、パスワードを解読します。
public class FingerPrintAuthHelper {
private static final String FINGER_PRINT_HELPER = "FingerPrintAuthHelper";
private static final String ENCRYPTED_PASS_SHARED_PREF_KEY = "ENCRYPTED_PASS_SHARED_PREF_KEY";
private static final String LAST_USED_IV_SHARED_PREF_KEY = "LAST_USED_IV_SHARED_PREF_KEY";
private static final String MY_APP_ALIAS = "MY_APP_ALIAS";
private KeyguardManager keyguardManager;
private FingerprintManager fingerprintManager;
private final Context context;
private KeyStore keyStore;
private KeyGenerator keyGenerator;
private String lastError;
public interface Callback {
void onSuccess(String savedPass);
void onFailure(String message);
void onHelp(int helpCode, String helpString);
}
public FingerPrintAuthHelper(Context context) {
this.context = context;
}
public String getLastError() {
return lastError;
}
@TargetApi(Build.VERSION_CODES.M)
public boolean init() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
setError("This Android version does not support fingerprint authentication");
return false;
}
keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE);
fingerprintManager = (FingerprintManager) context.getSystemService(FINGERPRINT_SERVICE);
if (!keyguardManager.isKeyguardSecure()) {
setError("User hasn't enabled Lock Screen");
return false;
}
if (!hasPermission()) {
setError("User hasn't granted permission to use Fingerprint");
return false;
}
if (!fingerprintManager.hasEnrolledFingerprints()) {
setError("User hasn't registered any fingerprints");
return false;
}
if (!initKeyStore()) {
return false;
}
return false;
}
@Nullable
@RequiresApi(api = Build.VERSION_CODES.M)
private Cipher createCipher(int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" +
KeyProperties.BLOCK_MODE_CBC + "/" +
KeyProperties.ENCRYPTION_PADDING_PKCS7);
Key key = keyStore.getKey(MY_APP_ALIAS, null);
if (key == null) {
return null;
}
if(mode == Cipher.ENCRYPT_MODE) {
cipher.init(mode, key);
byte[] iv = cipher.getIV();
saveIv(iv);
} else {
byte[] lastIv = getLastIv();
cipher.init(mode, key, new IvParameterSpec(lastIv));
}
return cipher;
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.M)
private KeyGenParameterSpec createKeyGenParameterSpec() {
return new KeyGenParameterSpec.Builder(MY_APP_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build();
}
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean initKeyStore() {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyStore.load(null);
if (getLastIv() == null) {
KeyGenParameterSpec keyGeneratorSpec = createKeyGenParameterSpec();
keyGenerator.init(keyGeneratorSpec);
keyGenerator.generateKey();
}
} catch (Throwable t) {
setError("Failed init of keyStore & keyGenerator: " + t.getMessage());
return false;
}
return true;
}
@RequiresApi(api = Build.VERSION_CODES.M)
private void authenticate(CancellationSignal cancellationSignal, FingerPrintAuthenticationListener authListener, int mode) {
try {
if (hasPermission()) {
Cipher cipher = createCipher(mode);
FingerprintManager.CryptoObject crypto = new FingerprintManager.CryptoObject(cipher);
fingerprintManager.authenticate(crypto, cancellationSignal, 0, authListener, null);
} else {
authListener.getCallback().onFailure("User hasn't granted permission to use Fingerprint");
}
} catch (Throwable t) {
authListener.getCallback().onFailure("An error occurred: " + t.getMessage());
}
}
private String getSavedEncryptedPassword() {
SharedPreferences sharedPreferences = getSharedPreferences();
if (sharedPreferences != null) {
return sharedPreferences.getString(ENCRYPTED_PASS_SHARED_PREF_KEY, null);
}
return null;
}
private void saveEncryptedPassword(String encryptedPassword) {
SharedPreferences.Editor edit = getSharedPreferences().edit();
edit.putString(ENCRYPTED_PASS_SHARED_PREF_KEY, encryptedPassword);
edit.commit();
}
private byte[] getLastIv() {
SharedPreferences sharedPreferences = getSharedPreferences();
if (sharedPreferences != null) {
String ivString = sharedPreferences.getString(LAST_USED_IV_SHARED_PREF_KEY, null);
if (ivString != null) {
return decodeBytes(ivString);
}
}
return null;
}
private void saveIv(byte[] iv) {
SharedPreferences.Editor edit = getSharedPreferences().edit();
String string = encodeBytes(iv);
edit.putString(LAST_USED_IV_SHARED_PREF_KEY, string);
edit.commit();
}
private SharedPreferences getSharedPreferences() {
return context.getSharedPreferences(FINGER_PRINT_HELPER, 0);
}
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean hasPermission() {
return ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT) == PackageManager.PERMISSION_GRANTED;
}
@RequiresApi(api = Build.VERSION_CODES.M)
public void savePassword(@NonNull String password, CancellationSignal cancellationSignal, Callback callback) {
authenticate(cancellationSignal, new FingerPrintEncryptPasswordListener(callback, password), Cipher.ENCRYPT_MODE);
}
@RequiresApi(api = Build.VERSION_CODES.M)
public void getPassword(CancellationSignal cancellationSignal, Callback callback) {
authenticate(cancellationSignal, new FingerPrintDecryptPasswordListener(callback), Cipher.DECRYPT_MODE);
}
@RequiresApi(api = Build.VERSION_CODES.M)
public boolean encryptPassword(Cipher cipher, String password) {
try {
// Encrypt the text
if(password.isEmpty()) {
setError("Password is empty");
return false;
}
if (cipher == null) {
setError("Could not create cipher");
return false;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
byte[] bytes = password.getBytes(Charset.defaultCharset());
cipherOutputStream.write(bytes);
cipherOutputStream.flush();
cipherOutputStream.close();
saveEncryptedPassword(encodeBytes(outputStream.toByteArray()));
} catch (Throwable t) {
setError("Encryption failed " + t.getMessage());
return false;
}
return true;
}
private byte[] decodeBytes(String s) {
final int len = s.length();
// "111" is not a valid hex encoding.
if( len%2 != 0 )
throw new IllegalArgumentException("hexBinary needs to be even-length: "+s);
byte[] out = new byte[len/2];
for( int i=0; i<len; i+=2 ) {
int h = hexToBin(s.charAt(i ));
int l = hexToBin(s.charAt(i+1));
if( h==-1 || l==-1 )
throw new IllegalArgumentException("contains illegal character for hexBinary: "+s);
out[i/2] = (byte)(h*16+l);
}
return out;
}
private static int hexToBin( char ch ) {
if( '0'<=ch && ch<='9' ) return ch-'0';
if( 'A'<=ch && ch<='F' ) return ch-'A'+10;
if( 'a'<=ch && ch<='f' ) return ch-'a'+10;
return -1;
}
private static final char[] hexCode = "0123456789ABCDEF".toCharArray();
public String encodeBytes(byte[] data) {
StringBuilder r = new StringBuilder(data.length*2);
for ( byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
@NonNull
private String decipher(Cipher cipher) throws IOException, IllegalBlockSizeException, BadPaddingException {
String retVal = null;
String savedEncryptedPassword = getSavedEncryptedPassword();
if (savedEncryptedPassword != null) {
byte[] decodedPassword = decodeBytes(savedEncryptedPassword);
CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(decodedPassword), cipher);
ArrayList<Byte> values = new ArrayList<>();
int nextByte;
while ((nextByte = cipherInputStream.read()) != -1) {
values.add((byte) nextByte);
}
cipherInputStream.close();
byte[] bytes = new byte[values.size()];
for (int i = 0; i < values.size(); i++) {
bytes[i] = values.get(i).byteValue();
}
retVal = new String(bytes, Charset.defaultCharset());
}
return retVal;
}
private void setError(String error) {
lastError = error;
Log.w(FINGER_PRINT_HELPER, lastError);
}
@RequiresApi(Build.VERSION_CODES.M)
protected class FingerPrintAuthenticationListener extends FingerprintManager.AuthenticationCallback {
protected final Callback callback;
public FingerPrintAuthenticationListener(@NonNull Callback callback) {
this.callback = callback;
}
public void onAuthenticationError(int errorCode, CharSequence errString) {
callback.onFailure("Authentication error [" + errorCode + "] " + errString);
}
/**
* Called when a recoverable error has been encountered during authentication. The help
* string is provided to give the user guidance for what went wrong, such as
* "Sensor dirty, please clean it."
* @param helpCode An integer identifying the error message
* @param helpString A human-readable string that can be shown in UI
*/
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
callback.onHelp(helpCode, helpString.toString());
}
/**
* Called when a fingerprint is recognized.
* @param result An object containing authentication-related data
*/
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
}
/**
* Called when a fingerprint is valid but not recognized.
*/
public void onAuthenticationFailed() {
callback.onFailure("Authentication failed");
}
public @NonNull
Callback getCallback() {
return callback;
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
private class FingerPrintEncryptPasswordListener extends FingerPrintAuthenticationListener {
private final String password;
public FingerPrintEncryptPasswordListener(Callback callback, String password) {
super(callback);
this.password = password;
}
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
Cipher cipher = result.getCryptoObject().getCipher();
try {
if (encryptPassword(cipher, password)) {
callback.onSuccess("Encrypted");
} else {
callback.onFailure("Encryption failed");
}
} catch (Exception e) {
callback.onFailure("Encryption failed " + e.getMessage());
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
protected class FingerPrintDecryptPasswordListener extends FingerPrintAuthenticationListener {
public FingerPrintDecryptPasswordListener(@NonNull Callback callback) {
super(callback);
}
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
Cipher cipher = result.getCryptoObject().getCipher();
try {
String savedPass = decipher(cipher);
if (savedPass != null) {
callback.onSuccess(savedPass);
} else {
callback.onFailure("Failed deciphering");
}
} catch (Exception e) {
callback.onFailure("Deciphering failed " + e.getMessage());
}
}
}
}
以下のこのアクティビティは、ユーザーがパスワードを保存してヘルパーと対話する方法の非常に基本的な例です。
public class MainActivity extends AppCompatActivity {
private TextView passwordTextView;
private FingerPrintAuthHelper fingerPrintAuthHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
passwordTextView = (TextView) findViewById(R.id.password);
errorTextView = (TextView) findViewById(R.id.error);
View savePasswordButton = findViewById(R.id.set_password_button);
savePasswordButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintAuthHelper.savePassword(passwordTextView.getText().toString(), new CancellationSignal(), getAuthListener(false));
}
}
});
View getPasswordButton = findViewById(R.id.get_password_button);
getPasswordButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintAuthHelper.getPassword(new CancellationSignal(), getAuthListener(true));
}
}
});
}
// Start the finger print helper. In case this fails show error to user
private void startFingerPrintAuthHelper() {
fingerPrintAuthHelper = new FingerPrintAuthHelper(this);
if (!fingerPrintAuthHelper.init()) {
errorTextView.setText(fingerPrintAuthHelper.getLastError());
}
}
@NonNull
private FingerPrintAuthHelper.Callback getAuthListener(final boolean isGetPass) {
return new FingerPrintAuthHelper.Callback() {
@Override
public void onSuccess(String result) {
if (isGetPass) {
errorTextView.setText("Success!!! Pass = " + result);
} else {
errorTextView.setText("Encrypted pass = " + result);
}
}
@Override
public void onFailure(String message) {
errorTextView.setText("Failed - " + message);
}
@Override
public void onHelp(int helpCode, String helpString) {
errorTextView.setText("Help needed - " + helpString);
}
};
}
}