@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 byte[] FIXED_IV = new byte[12];
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
- cipher.init(Cipher.ENCRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, FIXED_IV));
367
- byte[] encodedBytes = cipher.doFinal(stringToEncrypt.getBytes(StandardCharsets.UTF_8));
368
- return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
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[] encryptedData = Base64.decode(stringToDecrypt, Base64.DEFAULT);
385
+ byte[] combined = Base64.decode(stringToDecrypt, Base64.DEFAULT);
373
386
 
374
- Cipher cipher;
375
- cipher = Cipher.getInstance(TRANSFORMATION);
376
- cipher.init(Cipher.DECRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, FIXED_IV));
377
- byte[] decryptedData = cipher.doFinal(encryptedData);
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.2.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-native-biometric",
3
- "version": "8.2.0",
3
+ "version": "8.3.0",
4
4
  "description": "This plugin gives access to the native biometric apis for android and iOS",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",