Skip to content

Commit 83c7cc3

Browse files
committed
Merge branch 'dev' into staging
2 parents 442f257 + 361d5f0 commit 83c7cc3

50 files changed

Lines changed: 15400 additions & 10554 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lint.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Lint check
2+
3+
on:
4+
push:
5+
branches: [ main, dev ]
6+
pull_request:
7+
branches: [ main, dev ]
8+
9+
permissions:
10+
contents: read
11+
packages: read
12+
13+
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
submodules: recursive
22+
23+
- name: Setup Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: "22.x"
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Run ESLint
32+
run: npm run lint

.github/workflows/test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Test check
2+
3+
on:
4+
push:
5+
branches: [ main, dev ]
6+
pull_request:
7+
branches: [ main, dev ]
8+
9+
permissions:
10+
contents: read
11+
packages: read
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
submodules: recursive
22+
23+
- name: Setup Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: "22.x"
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Compile locale files
32+
run: npm run lingui:compile
33+
34+
- name: Run tests
35+
run: npm test

.npmrc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
legacy-peer-deps=true
2-
install-links=true
3-

android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ android {
9393
minSdkVersion rootProject.ext.minSdkVersion
9494
targetSdkVersion rootProject.ext.targetSdkVersion
9595
versionCode 1
96-
versionName "1.2.0"
96+
versionName "1.2.1"
9797
}
9898
signingConfigs {
9999
debug {

android/app/src/main/java/com/noxtton/pearpass/autofill/data/PearPassVaultClient.java

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,28 @@ public CompletableFuture<String> hashPassword(String password) {
959959
});
960960
}
961961

962+
/**
963+
* Hashes a password using secure byte buffer.
964+
* The password is converted to Base64 for transmission (matching JS pattern).
965+
*
966+
* @param password The password as byte array
967+
* @return CompletableFuture with the hashed password
968+
*/
969+
public CompletableFuture<String> hashPassword(byte[] password) {
970+
// Convert password to Base64 for transmission
971+
String passwordBase64 = com.pears.pass.autofill.utils.SecureBufferUtils.toBase64(password);
972+
973+
return sendRequest(API.ENCRYPTION_HASH_PASSWORD.getValue(), createMap("password", passwordBase64))
974+
.thenApply(result -> {
975+
String hashedPassword = (String) result.get("hashedPassword");
976+
if (hashedPassword == null) {
977+
throw new RuntimeException("Encryption operation failed");
978+
}
979+
log("Successfully hashed password (buffer)");
980+
return hashedPassword;
981+
});
982+
}
983+
962984
public CompletableFuture<DecryptionKeyResult> getDecryptionKey(String salt, String password) {
963985
Map<String, Object> params = new HashMap<>();
964986
params.put("salt", salt);
@@ -983,6 +1005,41 @@ public CompletableFuture<DecryptionKeyResult> getDecryptionKey(String salt, Stri
9831005
});
9841006
}
9851007

1008+
/**
1009+
* Gets the decryption key using secure byte buffer for password.
1010+
* The password is converted to Base64 for transmission (matching JS pattern).
1011+
*
1012+
* @param salt The salt to use for key derivation
1013+
* @param password The password as byte array
1014+
* @return CompletableFuture with the decryption key result
1015+
*/
1016+
public CompletableFuture<DecryptionKeyResult> getDecryptionKey(String salt, byte[] password) {
1017+
// Convert password to Base64 for transmission
1018+
String passwordBase64 = com.pears.pass.autofill.utils.SecureBufferUtils.toBase64(password);
1019+
1020+
Map<String, Object> params = new HashMap<>();
1021+
params.put("salt", salt);
1022+
params.put("password", passwordBase64);
1023+
1024+
return sendRequest(API.ENCRYPTION_GET_DECRYPTION_KEY.getValue(), params)
1025+
.thenApply(result -> {
1026+
String key = (String) result.get("value");
1027+
if (key == null) {
1028+
key = (String) result.get("key");
1029+
}
1030+
if (key == null) {
1031+
key = (String) result.get("hashedPassword");
1032+
}
1033+
if (key == null) {
1034+
logError("Failed to extract key from getDecryptionKey response: " + result);
1035+
throw new RuntimeException("Decryption failed");
1036+
}
1037+
1038+
log("Successfully generated decryption key (buffer)");
1039+
return new DecryptionKeyResult(key, salt);
1040+
});
1041+
}
1042+
9861043
public CompletableFuture<Map<String, Object>> decryptVaultKey(String ciphertext, String nonce, String hashedPassword) {
9871044
Map<String, Object> params = new HashMap<>();
9881045
params.put("ciphertext", ciphertext);
@@ -1243,6 +1300,205 @@ public CompletableFuture<Boolean> getVaultById(String vaultId, String password)
12431300
});
12441301
}
12451302

1303+
/**
1304+
* Validate vault password using secure byte buffer.
1305+
* The password is converted to Base64 for transmission (matching JS pattern).
1306+
*
1307+
* @param vaultId The vault ID to validate password for
1308+
* @param password The password as byte array
1309+
* @return CompletableFuture with true if password is valid, false otherwise
1310+
*/
1311+
public CompletableFuture<Boolean> validateVaultPassword(String vaultId, byte[] password) {
1312+
log("Validating password for vault (buffer): " + vaultId);
1313+
1314+
// Convert password to Base64 for transmission
1315+
String passwordBase64 = com.pears.pass.autofill.utils.SecureBufferUtils.toBase64(password);
1316+
1317+
return listVaults()
1318+
.thenCompose(vaults -> {
1319+
// Find the vault with the matching ID
1320+
Vault targetVault = null;
1321+
for (Vault vault : vaults) {
1322+
if (vault.id.equals(vaultId)) {
1323+
targetVault = vault;
1324+
break;
1325+
}
1326+
}
1327+
1328+
if (targetVault == null) {
1329+
throw new RuntimeException("Vault not found with ID: " + vaultId);
1330+
}
1331+
1332+
final Vault vault = targetVault;
1333+
1334+
// If vault has no encryption, it's not protected
1335+
if (vault.encryption == null) {
1336+
log("Vault " + vault.name + " is not protected, validation successful");
1337+
return CompletableFuture.completedFuture(true);
1338+
}
1339+
1340+
// Check if vault has its own salt (for password-protected vaults)
1341+
String saltToUse = vault.encryption.salt;
1342+
1343+
// If vault doesn't have salt, it's encrypted with master password
1344+
if (saltToUse == null || saltToUse.isEmpty()) {
1345+
log("Vault " + vault.name + " doesn't have its own salt, using master password");
1346+
return getMasterPasswordEncryption(null)
1347+
.thenCompose(masterPasswordEncryption -> {
1348+
if (masterPasswordEncryption == null || masterPasswordEncryption.hashedPassword == null) {
1349+
throw new RuntimeException("No master password available");
1350+
}
1351+
return decryptVaultKey(vault.encryption.ciphertext, vault.encryption.nonce, masterPasswordEncryption.hashedPassword);
1352+
})
1353+
.thenApply(decryptedData -> {
1354+
if (decryptedData == null) {
1355+
throw new RuntimeException("Failed to decrypt vault key");
1356+
}
1357+
String encryptionKey = extractValue(decryptedData, "value", "key", "data");
1358+
if (encryptionKey == null) {
1359+
throw new RuntimeException("Failed to decrypt vault key");
1360+
}
1361+
log("Password validation successful");
1362+
return true;
1363+
});
1364+
}
1365+
1366+
log("Vault " + vault.name + " has its own salt, using vault password (buffer)");
1367+
1368+
// Get decryption key using the vault's salt and password (Base64 encoded)
1369+
Map<String, Object> decryptionParams = new HashMap<>();
1370+
decryptionParams.put("password", passwordBase64);
1371+
decryptionParams.put("salt", saltToUse);
1372+
1373+
return sendRequest(API.ENCRYPTION_GET_DECRYPTION_KEY.getValue(), decryptionParams)
1374+
.thenCompose(decryptionResult -> {
1375+
String hashedPassword = extractValue(decryptionResult, "value", "key", "hashedPassword");
1376+
if (hashedPassword == null) {
1377+
throw new RuntimeException("Failed to get decryption key");
1378+
}
1379+
log("Got decryption key, attempting to decrypt vault key");
1380+
return decryptVaultKey(vault.encryption.ciphertext, vault.encryption.nonce, hashedPassword)
1381+
.thenApply(decryptedData -> {
1382+
if (decryptedData == null) {
1383+
throw new RuntimeException("Failed to decrypt vault key - incorrect password");
1384+
}
1385+
String encryptionKey = extractValue(decryptedData, "value", "key", "data");
1386+
if (encryptionKey == null) {
1387+
throw new RuntimeException("Failed to decrypt vault key - incorrect password");
1388+
}
1389+
log("Password validation successful (buffer)");
1390+
return true;
1391+
});
1392+
});
1393+
})
1394+
.exceptionally(ex -> {
1395+
log("Failed to validate vault password: " + ex.getMessage());
1396+
return false;
1397+
});
1398+
}
1399+
1400+
/**
1401+
* Get vault by ID and unlock it using secure byte buffer for password.
1402+
* The password is converted to Base64 for transmission (matching JS pattern).
1403+
*
1404+
* @param vaultId The vault ID to unlock
1405+
* @param password The password as byte array
1406+
* @return CompletableFuture with true if vault was unlocked, false otherwise
1407+
*/
1408+
public CompletableFuture<Boolean> getVaultById(String vaultId, byte[] password) {
1409+
log("Getting vault by ID (buffer): " + vaultId);
1410+
1411+
// Convert password to Base64 for transmission
1412+
String passwordBase64 = com.pears.pass.autofill.utils.SecureBufferUtils.toBase64(password);
1413+
1414+
return listVaults()
1415+
.thenCompose(vaults -> {
1416+
// Find the vault with the matching ID
1417+
Vault targetVault = null;
1418+
for (Vault vault : vaults) {
1419+
if (vault.id.equals(vaultId)) {
1420+
targetVault = vault;
1421+
break;
1422+
}
1423+
}
1424+
1425+
if (targetVault == null) {
1426+
throw new RuntimeException("Vault not found with ID: " + vaultId);
1427+
}
1428+
1429+
final Vault vault = targetVault;
1430+
1431+
// If vault has no encryption, it's not protected
1432+
if (vault.encryption == null) {
1433+
log("Vault " + vault.name + " is not protected, initializing directly");
1434+
return activeVaultInit(vault.id, null)
1435+
.thenApply(result -> true);
1436+
}
1437+
1438+
// Check if vault has its own salt (for password-protected vaults)
1439+
String saltToUse = vault.encryption.salt;
1440+
1441+
// If vault doesn't have salt, it's encrypted with master password
1442+
if (saltToUse == null || saltToUse.isEmpty()) {
1443+
log("Vault " + vault.name + " doesn't have its own salt, using master password");
1444+
return getMasterPasswordEncryption(null)
1445+
.thenCompose(masterPasswordEncryption -> {
1446+
if (masterPasswordEncryption == null || masterPasswordEncryption.hashedPassword == null) {
1447+
throw new RuntimeException("No master password available");
1448+
}
1449+
return decryptVaultKey(vault.encryption.ciphertext, vault.encryption.nonce, masterPasswordEncryption.hashedPassword);
1450+
})
1451+
.thenCompose(decryptedData -> {
1452+
if (decryptedData == null) {
1453+
throw new RuntimeException("Failed to decrypt vault key");
1454+
}
1455+
String encryptionKey = extractValue(decryptedData, "value", "key", "data");
1456+
if (encryptionKey == null) {
1457+
throw new RuntimeException("Failed to decrypt vault key");
1458+
}
1459+
return activeVaultInit(vault.id, encryptionKey)
1460+
.thenApply(result -> true);
1461+
});
1462+
}
1463+
1464+
log("Vault " + vault.name + " has its own salt, using vault password (buffer)");
1465+
1466+
// Get decryption key using the vault's salt and password (Base64 encoded)
1467+
Map<String, Object> decryptionParams = new HashMap<>();
1468+
decryptionParams.put("password", passwordBase64);
1469+
decryptionParams.put("salt", saltToUse);
1470+
1471+
return sendRequest(API.ENCRYPTION_GET_DECRYPTION_KEY.getValue(), decryptionParams)
1472+
.thenCompose(decryptionResult -> {
1473+
String hashedPassword = extractValue(decryptionResult, "value", "key", "hashedPassword");
1474+
if (hashedPassword == null) {
1475+
throw new RuntimeException("Failed to get decryption key");
1476+
}
1477+
log("Got decryption key, attempting to decrypt vault key (buffer)");
1478+
return decryptVaultKey(vault.encryption.ciphertext, vault.encryption.nonce, hashedPassword)
1479+
.thenCompose(decryptedData -> {
1480+
if (decryptedData == null) {
1481+
throw new RuntimeException("Failed to decrypt vault key - incorrect password");
1482+
}
1483+
String encryptionKey = extractValue(decryptedData, "value", "key", "data");
1484+
if (encryptionKey == null) {
1485+
throw new RuntimeException("Failed to decrypt vault key - incorrect password");
1486+
}
1487+
log("Successfully decrypted vault key, initializing active vault (buffer)");
1488+
return activeVaultInit(vault.id, encryptionKey)
1489+
.thenApply(result -> {
1490+
log("Active vault initialized successfully (buffer)");
1491+
return true;
1492+
});
1493+
});
1494+
});
1495+
})
1496+
.exceptionally(ex -> {
1497+
log("Failed to get vault by ID: " + ex.getMessage());
1498+
return false;
1499+
});
1500+
}
1501+
12461502
// General Methods
12471503
public CompletableFuture<Map<String, Object>> closeAllInstances() {
12481504
log("Closing all vault instances and cleaning up resources");

android/app/src/main/java/com/noxtton/pearpass/autofill/ui/MasterPasswordFragment.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ private void authenticateWithMasterPassword(String password) {
103103
unlockButton.setEnabled(false);
104104
unlockButton.setText("Unlocking...");
105105

106+
// Convert password to byte array for secure handling
107+
byte[] passwordBuffer = com.pears.pass.autofill.utils.SecureBufferUtils.stringToBuffer(password);
108+
106109
CompletableFuture.runAsync(() -> {
107110
try {
108111
// Get the master password encryption to get salt
@@ -113,9 +116,9 @@ private void authenticateWithMasterPassword(String password) {
113116
throw new Exception("No master password configuration found");
114117
}
115118

116-
// Get decryption key from password and salt
119+
// Get decryption key from password buffer and salt (using byte[] version)
117120
PearPassVaultClient.DecryptionKeyResult decryptionKey =
118-
vaultClient.getDecryptionKey(masterPasswordEncryption.salt, password).get();
121+
vaultClient.getDecryptionKey(masterPasswordEncryption.salt, passwordBuffer).get();
119122

120123
// Decrypt the vault key using the hashed password
121124
Map<String, Object> decryptResult = vaultClient.decryptVaultKey(
@@ -162,6 +165,9 @@ private void authenticateWithMasterPassword(String password) {
162165
unlockButton.setText("Unlock");
163166
passwordInput.setText("");
164167
});
168+
} finally {
169+
// Securely clear the password buffer
170+
com.pears.pass.autofill.utils.SecureBufferUtils.clearBuffer(passwordBuffer);
165171
}
166172
});
167173
}

0 commit comments

Comments
 (0)