@capgo/capacitor-native-biometric 8.2.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -119,6 +119,17 @@ This plugin does NOT provide:
|
|
|
119
119
|
- ❌ Server-side authentication or validation
|
|
120
120
|
- ❌ Root/jailbreak detection (use [@capgo/capacitor-is-root](https://github.com/Cap-go/capacitor-is-root))
|
|
121
121
|
|
|
122
|
+
### Recent Security Improvements (v8.2.0+)
|
|
123
|
+
|
|
124
|
+
**Android Encryption Enhancement**: The Android implementation now uses properly randomized Initialization Vectors (IVs) for AES-GCM encryption of stored credentials. Previous versions used a fixed IV, which is a cryptographic vulnerability.
|
|
125
|
+
|
|
126
|
+
**Automatic Migration**: The plugin automatically handles credentials encrypted with the older method:
|
|
127
|
+
- When reading credentials, it first attempts the new secure format, then falls back to the legacy format if needed
|
|
128
|
+
- When saving credentials, they are always encrypted using the new secure format
|
|
129
|
+
- No action required from users - migration happens transparently on first credential save after update
|
|
130
|
+
|
|
131
|
+
**Recommendation**: After updating to v8.2.0+, users should re-save their credentials to ensure they're encrypted with the improved format. This happens automatically when users authenticate and save credentials again.
|
|
132
|
+
|
|
122
133
|
## Installation (Only supports Capacitor 7)
|
|
123
134
|
|
|
124
135
|
- `npm i @capgo/capacitor-native-biometric`
|
|
@@ -40,6 +40,7 @@ import java.security.UnrecoverableEntryException;
|
|
|
40
40
|
import java.security.cert.CertificateException;
|
|
41
41
|
import java.util.ArrayList;
|
|
42
42
|
import java.util.Objects;
|
|
43
|
+
import javax.crypto.BadPaddingException;
|
|
43
44
|
import javax.crypto.Cipher;
|
|
44
45
|
import javax.crypto.CipherInputStream;
|
|
45
46
|
import javax.crypto.CipherOutputStream;
|
|
@@ -73,7 +74,7 @@ public class NativeBiometric extends Plugin {
|
|
|
73
74
|
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
|
74
75
|
private static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
|
|
75
76
|
private static final String AES_MODE = "AES/ECB/PKCS7Padding";
|
|
76
|
-
private static final
|
|
77
|
+
private static final int GCM_IV_LENGTH = 12;
|
|
77
78
|
private static final String ENCRYPTED_KEY = "NativeBiometricKey";
|
|
78
79
|
private static final String NATIVE_BIOMETRIC_SHARED_PREFERENCES = "NativeBiometricSharedPreferences";
|
|
79
80
|
|
|
@@ -363,18 +364,58 @@ public class NativeBiometric extends Plugin {
|
|
|
363
364
|
private String encryptString(String stringToEncrypt, String KEY_ALIAS) throws GeneralSecurityException, IOException {
|
|
364
365
|
Cipher cipher;
|
|
365
366
|
cipher = Cipher.getInstance(TRANSFORMATION);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
|
|
368
|
+
// Generate a random IV for each encryption operation
|
|
369
|
+
byte[] iv = new byte[GCM_IV_LENGTH];
|
|
370
|
+
SecureRandom secureRandom = new SecureRandom();
|
|
371
|
+
secureRandom.nextBytes(iv);
|
|
372
|
+
|
|
373
|
+
cipher.init(Cipher.ENCRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, iv));
|
|
374
|
+
byte[] encryptedBytes = cipher.doFinal(stringToEncrypt.getBytes(StandardCharsets.UTF_8));
|
|
375
|
+
|
|
376
|
+
// Prepend IV to the encrypted data
|
|
377
|
+
byte[] combined = new byte[iv.length + encryptedBytes.length];
|
|
378
|
+
System.arraycopy(iv, 0, combined, 0, iv.length);
|
|
379
|
+
System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length);
|
|
380
|
+
|
|
381
|
+
return Base64.encodeToString(combined, Base64.DEFAULT);
|
|
369
382
|
}
|
|
370
383
|
|
|
371
384
|
private String decryptString(String stringToDecrypt, String KEY_ALIAS) throws GeneralSecurityException, IOException {
|
|
372
|
-
byte[]
|
|
385
|
+
byte[] combined = Base64.decode(stringToDecrypt, Base64.DEFAULT);
|
|
373
386
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
387
|
+
// Try new format first (IV prepended to ciphertext)
|
|
388
|
+
// New format: 12-byte IV + ciphertext (plaintext + 16-byte GCM auth tag)
|
|
389
|
+
// We check for > GCM_IV_LENGTH to ensure there's at least some ciphertext beyond just the IV
|
|
390
|
+
// The cipher's doFinal() will validate the auth tag and fail if data is malformed
|
|
391
|
+
if (combined.length >= GCM_IV_LENGTH + 1) {
|
|
392
|
+
try {
|
|
393
|
+
// Extract IV from the beginning of the data
|
|
394
|
+
byte[] iv = new byte[GCM_IV_LENGTH];
|
|
395
|
+
byte[] encryptedData = new byte[combined.length - GCM_IV_LENGTH];
|
|
396
|
+
System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
|
|
397
|
+
System.arraycopy(combined, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
|
|
398
|
+
|
|
399
|
+
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
|
400
|
+
cipher.init(Cipher.DECRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, iv));
|
|
401
|
+
byte[] decryptedData = cipher.doFinal(encryptedData);
|
|
402
|
+
return new String(decryptedData, StandardCharsets.UTF_8);
|
|
403
|
+
} catch (BadPaddingException e) {
|
|
404
|
+
// Authentication tag verification failed (AEADBadTagException) or padding error
|
|
405
|
+
// BadPaddingException is the parent class of AEADBadTagException
|
|
406
|
+
// Likely means data was encrypted with legacy format - fall through to legacy decryption
|
|
407
|
+
} catch (GeneralSecurityException e) {
|
|
408
|
+
// Other security exceptions should not be masked - rethrow
|
|
409
|
+
throw e;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fallback to legacy format (FIXED_IV - all zeros)
|
|
414
|
+
// This branch handles credentials encrypted with the old vulnerable method
|
|
415
|
+
byte[] LEGACY_FIXED_IV = new byte[12]; // All zeros by default
|
|
416
|
+
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
|
417
|
+
cipher.init(Cipher.DECRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, LEGACY_FIXED_IV));
|
|
418
|
+
byte[] decryptedData = cipher.doFinal(combined);
|
|
378
419
|
return new String(decryptedData, StandardCharsets.UTF_8);
|
|
379
420
|
}
|
|
380
421
|
|
|
@@ -410,8 +451,7 @@ public class NativeBiometric extends Plugin {
|
|
|
410
451
|
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
|
|
411
452
|
)
|
|
412
453
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
413
|
-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
414
|
-
.setRandomizedEncryptionRequired(false);
|
|
454
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
|
|
415
455
|
|
|
416
456
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
417
457
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || Build.VERSION.SDK_INT > 34) {
|
|
@@ -11,7 +11,7 @@ import LocalAuthentication
|
|
|
11
11
|
|
|
12
12
|
@objc(NativeBiometricPlugin)
|
|
13
13
|
public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
14
|
-
private let pluginVersion: String = "8.
|
|
14
|
+
private let pluginVersion: String = "8.3.0"
|
|
15
15
|
public let identifier = "NativeBiometricPlugin"
|
|
16
16
|
public let jsName = "NativeBiometric"
|
|
17
17
|
public let pluginMethods: [CAPPluginMethod] = [
|
package/package.json
CHANGED