From 0cc9bfbce18621e8443286b25de8746d19dd3faf Mon Sep 17 00:00:00 2001 From: zHd4 <38856321+zHd4@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:23:13 +0200 Subject: [PATCH 1/6] Add method to check for non-zero characters in char array --- .../main/java/app/notesr/core/util/CharUtils.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/src/main/java/app/notesr/core/util/CharUtils.java b/core/src/main/java/app/notesr/core/util/CharUtils.java index 13a6d326..660c1bc8 100644 --- a/core/src/main/java/app/notesr/core/util/CharUtils.java +++ b/core/src/main/java/app/notesr/core/util/CharUtils.java @@ -63,4 +63,18 @@ public static char[] bytesToChars(byte[] bytes, Charset charset) return chars; } + + public static boolean hasNonZeroChars(char[] chars) { + if (chars == null) { + return false; + } + + for (char c : chars) { + if (c != '\0') { + return true; + } + } + + return false; + } } From de507de5c45963065b48c4a8cfa5078cfba809be Mon Sep 17 00:00:00 2001 From: zHd4 <38856321+zHd4@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:23:38 +0200 Subject: [PATCH 2/6] Add tests for hasNonZeroChars method in CharUtils --- .../app/notesr/core/util/CharUtilsTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/core/src/test/java/app/notesr/core/util/CharUtilsTest.java b/core/src/test/java/app/notesr/core/util/CharUtilsTest.java index f208c5c2..bd69b575 100644 --- a/core/src/test/java/app/notesr/core/util/CharUtilsTest.java +++ b/core/src/test/java/app/notesr/core/util/CharUtilsTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -69,4 +70,81 @@ void testConversionWithSpecialCharacters() throws CharacterCodingException { char[] result = CharUtils.bytesToChars(bytes, StandardCharsets.UTF_8); assertArrayEquals(original, result, "Conversion should preserve all special characters"); } + + @Test + void testHasNonZeroCharsWithNull() { + boolean result = CharUtils.hasNonZeroChars(null); + assertFalse(result, "hasNonZeroChars should return false for null input"); + } + + @Test + void testHasNonZeroCharsWithEmptyArray() { + char[] emptyArray = new char[0]; + boolean result = CharUtils.hasNonZeroChars(emptyArray); + assertFalse(result, "hasNonZeroChars should return false for empty array"); + } + + @Test + void testHasNonZeroCharsWithAllZeroChars() { + char[] allZeros = {'\0', '\0', '\0'}; + boolean result = CharUtils.hasNonZeroChars(allZeros); + assertFalse(result, + "hasNonZeroChars should return false when all characters are null"); + } + + @Test + void testHasNonZeroCharsWithSingleZeroChar() { + char[] singleZero = {'\0'}; + boolean result = CharUtils.hasNonZeroChars(singleZero); + assertFalse(result, + "hasNonZeroChars should return false for single null character"); + } + + @Test + void testHasNonZeroCharsWithNonZeroAtStart() { + char[] chars = {'a', '\0', '\0'}; + boolean result = CharUtils.hasNonZeroChars(chars); + assertTrue(result, + "hasNonZeroChars should return true when non-zero char is at start"); + } + + @Test + void testHasNonZeroCharsWithNonZeroAtEnd() { + char[] chars = {'\0', '\0', 'z'}; + boolean result = CharUtils.hasNonZeroChars(chars); + assertTrue(result, + "hasNonZeroChars should return true when non-zero char is at end"); + } + + @Test + void testHasNonZeroCharsWithNonZeroInMiddle() { + char[] chars = {'\0', 'b', '\0'}; + boolean result = CharUtils.hasNonZeroChars(chars); + assertTrue(result, + "hasNonZeroChars should return true when non-zero char is in middle"); + } + + @Test + void testHasNonZeroCharsWithAllNonZeroChars() { + char[] allNonZero = {'H', 'e', 'l', 'l', 'o'}; + boolean result = CharUtils.hasNonZeroChars(allNonZero); + assertTrue(result, + "hasNonZeroChars should return true when all characters are non-zero"); + } + + @Test + void testHasNonZeroCharsWithSingleNonZeroChar() { + char[] singleNonZero = {'x'}; + boolean result = CharUtils.hasNonZeroChars(singleNonZero); + assertTrue(result, + "hasNonZeroChars should return true for single non-zero character"); + } + + @Test + void testHasNonZeroCharsWithSpecialCharacters() { + char[] specialChars = {'\0', 'ñ', '\0'}; + boolean result = CharUtils.hasNonZeroChars(specialChars); + assertTrue(result, + "hasNonZeroChars should return true for special non-zero characters"); + } } From 20a6e39b24af52c91a4a7e9016069c054c761bf6 Mon Sep 17 00:00:00 2001 From: zHd4 <38856321+zHd4@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:27:26 +0200 Subject: [PATCH 3/6] Add method to check if key consists of zeros --- .../java/app/notesr/core/util/KeyUtils.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/app/notesr/core/util/KeyUtils.java b/core/src/main/java/app/notesr/core/util/KeyUtils.java index 8f1729e8..33899e58 100644 --- a/core/src/main/java/app/notesr/core/util/KeyUtils.java +++ b/core/src/main/java/app/notesr/core/util/KeyUtils.java @@ -85,10 +85,25 @@ public static byte[] getIvFromSecrets(CryptoSecrets cryptoSecrets) { int ivSize = cryptoSecrets.getKey().length - (AesCryptor.KEY_SIZE / 8); byte[] iv = new byte[ivSize]; - System.arraycopy(cryptoSecrets.getKey(), AesCryptor.KEY_SIZE / 8, iv, 0, ivSize); + System.arraycopy(cryptoSecrets.getKey(), AesCryptor.KEY_SIZE / 8, iv, 0, + ivSize); return iv; } + public static boolean isKeyNulled(byte[] key) { + if (key == null) { + return false; + } + + for (byte c : key) { + if (c != 0) { + return false; + } + } + + return true; + } + private static String byteToHex(byte value) { char[] hexDigits = new char[2]; From be0af780c8e89a59a241e9a3e330a7300541b60b Mon Sep 17 00:00:00 2001 From: zHd4 <38856321+zHd4@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:28:19 +0200 Subject: [PATCH 4/6] Add tests for isKeyNulled method in KeyUtils --- .../app/notesr/core/util/KeyUtilsTest.java | 115 +++++++++++++++--- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/core/src/test/java/app/notesr/core/util/KeyUtilsTest.java b/core/src/test/java/app/notesr/core/util/KeyUtilsTest.java index 05af75d9..3034b0f1 100644 --- a/core/src/test/java/app/notesr/core/util/KeyUtilsTest.java +++ b/core/src/test/java/app/notesr/core/util/KeyUtilsTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,10 +35,12 @@ void getSecretKeyFromSecretsTruncatesOrPadsCorrectly() { CryptoSecrets secrets = new CryptoSecrets(longKey, "pass".toCharArray()); SecretKey secretKey = KeyUtils.getSecretKeyFromSecrets(secrets); - assertEquals(AesCryptor.KEY_GENERATOR_ALGORITHM, secretKey.getAlgorithm()); + assertEquals(AesCryptor.KEY_GENERATOR_ALGORITHM, secretKey.getAlgorithm(), + "SecretKey algorithm should match expected algorithm"); byte[] expected = Arrays.copyOfRange(longKey, 0, keyLength); - assertArrayEquals(expected, secretKey.getEncoded()); + assertArrayEquals(expected, secretKey.getEncoded(), + "SecretKey should be truncated to correct length"); } @Test @@ -53,8 +56,10 @@ void getSecretKeyFromSecretsPadsWithZerosIfShorter() { byte[] expected = new byte[keyLength]; Arrays.fill(expected, 0, shortKey.length, (byte) 0x5A); - assertEquals(AesCryptor.KEY_GENERATOR_ALGORITHM, secretKey.getAlgorithm()); - assertArrayEquals(expected, secretKey.getEncoded()); + assertEquals(AesCryptor.KEY_GENERATOR_ALGORITHM, secretKey.getAlgorithm(), + "SecretKey algorithm should match expected algorithm"); + assertArrayEquals(expected, secretKey.getEncoded(), + "Short key should be padded with zeros to correct length"); } @Test @@ -68,7 +73,8 @@ void getKeyHexFromSecretsFormatsCorrectly() { String expected = "01 23 45 67 \n89 AB CD EF"; String actual = KeyUtils.getHexFromKeyBytes(key); - assertEquals(expected, actual); + assertEquals(expected, actual, + "Hex conversion should format key with spaces and newlines correctly"); } @Test @@ -82,7 +88,8 @@ void getKeyBytesFromHexParsesCorrectly() { }; byte[] actual = KeyUtils.getKeyBytesFromHex(hex); - assertArrayEquals(expected, actual); + assertArrayEquals(expected, actual, + "Hex parsing should correctly convert formatted hex string to bytes"); } @Test @@ -92,7 +99,8 @@ void getKeyBytesFromHexHandlesExtraWhitespace() { byte[] expected = new byte[]{0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f}; byte[] actual = KeyUtils.getKeyBytesFromHex(hex); - assertArrayEquals(expected, actual); + assertArrayEquals(expected, actual, + "Hex parsing should handle extra whitespace (spaces, tabs, newlines)"); } @Test @@ -100,19 +108,23 @@ void getKeyBytesFromHexThrowsOnInvalidHex() { char[] invalidHex = "zz yy xx".toCharArray(); Exception exception = assertThrows(IllegalArgumentException.class, () -> - KeyUtils.getKeyBytesFromHex(invalidHex)); + KeyUtils.getKeyBytesFromHex(invalidHex), + "Should throw IllegalArgumentException for invalid hex characters"); - assertNotNull(exception.getMessage()); - assertTrue(exception.getMessage().contains("Invalid hex key")); + assertNotNull(exception.getMessage(), "Exception message should not be null"); + assertTrue(exception.getMessage().contains("Invalid hex key"), + "Exception message should contain 'Invalid hex key'"); } @Test void getKeyBytesFromHexThrowsOnNullInput() { Exception exception = assertThrows(NullPointerException.class, () -> - KeyUtils.getKeyBytesFromHex(null)); + KeyUtils.getKeyBytesFromHex(null), + "Should throw NullPointerException for null hex input"); - assertNotNull(exception.getMessage()); - assertTrue(exception.getMessage().contains("hexChars must not be null")); + assertNotNull(exception.getMessage(), "Exception message should not be null"); + assertTrue(exception.getMessage().contains("hexChars must not be null"), + "Exception message should contain 'hexChars must not be null'"); } @Test @@ -122,7 +134,8 @@ void hexConversionRoundTrip() { String hex = KeyUtils.getHexFromKeyBytes(originalKey); byte[] restoredKey = KeyUtils.getKeyBytesFromHex(hex.toCharArray()); - assertArrayEquals(originalKey, restoredKey); + assertArrayEquals(originalKey, restoredKey, + "Round-trip hex conversion should restore original key bytes"); } @Test @@ -130,7 +143,8 @@ void getHexFromCryptoSecretsDelegatesCorrectly() { CryptoSecrets secrets = new CryptoSecrets(new byte[]{0x0A, 0x0B}, "password".toCharArray()); String hex = KeyUtils.getKeyHexFromSecrets(secrets); - assertEquals("0A 0B", hex); + assertEquals("0A 0B", hex, + "getKeyHexFromSecrets should correctly delegate to getHexFromKeyBytes"); } @Test @@ -140,8 +154,10 @@ void getSecretsFromHexProducesCorrectObject() { CryptoSecrets secrets = KeyUtils.getSecretsFromHex(hex, password.toCharArray()); - assertArrayEquals(new byte[]{0x0C, 0x0D, 0x0E}, secrets.getKey()); - assertArrayEquals(password.toCharArray(), secrets.getPassword()); + assertArrayEquals(new byte[]{0x0C, 0x0D, 0x0E}, secrets.getKey(), + "CryptoSecrets key should match parsed hex"); + assertArrayEquals(password.toCharArray(), secrets.getPassword(), + "CryptoSecrets password should match input password"); } @Test @@ -157,7 +173,68 @@ void getIvExtractsIvCorrectly() { CryptoSecrets secrets = new CryptoSecrets(key, "password".toCharArray()); byte[] iv = KeyUtils.getIvFromSecrets(secrets); - assertEquals(ivLength, iv.length); - assertArrayEquals(Arrays.copyOfRange(key, keyLength - ivLength, keyLength), iv); + assertEquals(ivLength, iv.length, "Extracted IV should have correct length"); + assertArrayEquals(Arrays.copyOfRange(key, keyLength - ivLength, keyLength), iv, + "IV should be extracted from end of key"); + } + + @Test + void isKeyNulledReturnsFalseForNull() { + assertFalse(KeyUtils.isKeyNulled(null), + "isKeyNulled should return false for null input"); + } + + @Test + void isKeyNulledReturnsTrueForEmptyArray() { + byte[] emptyKey = new byte[0]; + assertTrue(KeyUtils.isKeyNulled(emptyKey), + "isKeyNulled should return true for empty array"); + } + + @Test + void isKeyNulledReturnsTrueForAllZeros() { + byte[] nulledKey = new byte[32]; + Arrays.fill(nulledKey, (byte) 0); + + assertTrue(KeyUtils.isKeyNulled(nulledKey), + "isKeyNulled should return true for array filled with zeros"); + } + + @Test + void isKeyNulledReturnsFalseForSingleNonZeroByte() { + byte[] key = new byte[32]; + Arrays.fill(key, (byte) 0); + key[15] = (byte) 1; + + assertFalse(KeyUtils.isKeyNulled(key), + "isKeyNulled should return false when array contains a single non-zero byte"); + } + + @Test + void isKeyNulledReturnsFalseForNonZeroAtStart() { + byte[] key = new byte[16]; + key[0] = (byte) 0xFF; + Arrays.fill(key, 1, key.length, (byte) 0); + + assertFalse(KeyUtils.isKeyNulled(key), + "isKeyNulled should return false when array has non-zero byte at start"); + } + + @Test + void isKeyNulledReturnsFalseForNonZeroAtEnd() { + byte[] key = new byte[16]; + Arrays.fill(key, 0, key.length - 1, (byte) 0); + key[key.length - 1] = (byte) 0x80; + + assertFalse(KeyUtils.isKeyNulled(key), + "isKeyNulled should return false when array has non-zero byte at end"); + } + + @Test + void isKeyNulledReturnsFalseForMultipleNonZeroBytes() { + byte[] key = new byte[]{0x01, 0x00, 0x02, 0x00, 0x03}; + + assertFalse(KeyUtils.isKeyNulled(key), + "isKeyNulled should return false when array contains multiple non-zero bytes"); } } From c1ba82ddf27a12b054cf89863d8f1d8988b4b082 Mon Sep 17 00:00:00 2001 From: zHd4 <38856321+zHd4@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:28:47 +0200 Subject: [PATCH 5/6] Add self-validation method for CryptoSecrets --- .../core/security/dto/CryptoSecrets.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/src/main/java/app/notesr/core/security/dto/CryptoSecrets.java b/core/src/main/java/app/notesr/core/security/dto/CryptoSecrets.java index c907e52f..d21d23fd 100644 --- a/core/src/main/java/app/notesr/core/security/dto/CryptoSecrets.java +++ b/core/src/main/java/app/notesr/core/security/dto/CryptoSecrets.java @@ -7,6 +7,8 @@ import java.util.Arrays; +import app.notesr.core.util.CharUtils; +import app.notesr.core.util.KeyUtils; import lombok.AllArgsConstructor; import lombok.Data; @@ -20,6 +22,9 @@ @Data public final class CryptoSecrets { + public static final int MASTER_KEY_SIZE = 48; + public static final int PASSWORD_MIN_LENGTH = 4; + /** * The cryptographic 384-bit master key (48 bytes). */ @@ -38,6 +43,43 @@ public void destroy() { Arrays.fill(password, '\0'); } + /** + * Validates the integrity of the cryptographic secrets. + *

+ * Ensures that both the key and password arrays are not null or empty, + * and that the key matches the expected 384-bit (48 bytes) length requirement + * and that the password is at least 4 characters long. + * + * @throws IllegalArgumentException if any validation check fails + */ + public void validate() { + if (key == null || key.length == 0) { + throw new IllegalStateException("CryptoSecrets key cannot be null or empty"); + } + + if (password == null || password.length == 0) { + throw new IllegalStateException("CryptoSecrets password cannot be null or empty"); + } + + if (key.length != MASTER_KEY_SIZE) { + throw new IllegalStateException("Key must be " + + MASTER_KEY_SIZE + " bytes long"); + } + + if (password.length < PASSWORD_MIN_LENGTH) { + throw new IllegalStateException("Password must be at least " + + PASSWORD_MIN_LENGTH + " characters long"); + } + + if (KeyUtils.isKeyNulled(key)) { + throw new IllegalStateException("Key cannot be empty"); + } + + if (!CharUtils.hasNonZeroChars(password)) { + throw new IllegalStateException("Password cannot be empty"); + } + } + /** * Creates a deep copy of the provided {@link CryptoSecrets} instance. * From 712dc9a5bc94e23558743106e6d038e50f5f47e4 Mon Sep 17 00:00:00 2001 From: zHd4 <38856321+zHd4@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:30:35 +0200 Subject: [PATCH 6/6] Remove KEY_SIZE field from CryptoManager --- .../java/app/notesr/NotesIntegrationTest.java | 3 ++- .../app/notesr/core/security/crypto/CryptoManager.java | 6 +----- .../notesr/core/security/crypto/CryptoManagerTest.java | 9 +++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/app/notesr/NotesIntegrationTest.java b/app/src/androidTest/java/app/notesr/NotesIntegrationTest.java index dc34a56c..03d5074e 100644 --- a/app/src/androidTest/java/app/notesr/NotesIntegrationTest.java +++ b/app/src/androidTest/java/app/notesr/NotesIntegrationTest.java @@ -45,6 +45,7 @@ import java.time.temporal.ChronoUnit; public class NotesIntegrationTest { + private static final int KEY_SIZE = 48; private static final SecureRandom RANDOM = new SecureRandom(); private static final Faker FAKER = new Faker(); @@ -223,7 +224,7 @@ public void testDeleteNote() throws Exception { } private static CryptoSecrets getTestSecrets() { - byte[] key = new byte[CryptoManager.KEY_SIZE]; + byte[] key = new byte[KEY_SIZE]; RANDOM.nextBytes(key); String password = FAKER.internet.password(); diff --git a/core/src/main/java/app/notesr/core/security/crypto/CryptoManager.java b/core/src/main/java/app/notesr/core/security/crypto/CryptoManager.java index aa1b64f8..a0b564fa 100644 --- a/core/src/main/java/app/notesr/core/security/crypto/CryptoManager.java +++ b/core/src/main/java/app/notesr/core/security/crypto/CryptoManager.java @@ -33,10 +33,6 @@ @RequiredArgsConstructor public final class CryptoManager { - /** - * The size of the master key in bytes. - */ - public static final int KEY_SIZE = 48; private static final String KEY_HASH_PREF = "key_hash"; private static final String BLOCK_MARKER_PREF = "is_blocked"; private static final String ENCRYPTED_KEY_FILENAME = "key.encrypted"; @@ -106,7 +102,7 @@ public boolean isBlocked(Context context) { * @return A new {@link CryptoSecrets} instance. */ public CryptoSecrets generateSecrets(char[] password) { - byte[] key = new byte[KEY_SIZE]; + byte[] key = new byte[CryptoSecrets.MASTER_KEY_SIZE]; secureRandom.nextBytes(key); return new CryptoSecrets(Arrays.copyOf(key, key.length), password); } diff --git a/core/src/test/java/app/notesr/core/security/crypto/CryptoManagerTest.java b/core/src/test/java/app/notesr/core/security/crypto/CryptoManagerTest.java index c6a8e3d2..f93a24e5 100644 --- a/core/src/test/java/app/notesr/core/security/crypto/CryptoManagerTest.java +++ b/core/src/test/java/app/notesr/core/security/crypto/CryptoManagerTest.java @@ -45,6 +45,7 @@ @ExtendWith(MockitoExtension.class) class CryptoManagerTest { + private static final int MASTER_KEY_SIZE = 48; private static final SecureRandom SECURE_RANDOM = new SecureRandom(new byte[]{1, 2, 3}); @Mock @@ -72,7 +73,7 @@ void setUp() { @Test void testGenerateSecretsCreatesKeyOfCorrectSize() { CryptoSecrets secrets = cryptoManager.generateSecrets("pass".toCharArray()); - assertEquals(CryptoManager.KEY_SIZE, secrets.getKey().length, + assertEquals(MASTER_KEY_SIZE, secrets.getKey().length, "Generated key size should match constant"); assertArrayEquals("pass".toCharArray(), secrets.getPassword(), "Generated secrets should contain the provided password"); @@ -195,7 +196,7 @@ void testSetKeyHashDoubleHashingProblem() throws Exception { void testConfigureSuccess() throws Exception { char[] password = "password".toCharArray(); byte[] encryptedKey = new byte[]{1, 2, 3}; - byte[] decryptedKey = new byte[CryptoManager.KEY_SIZE]; + byte[] decryptedKey = new byte[MASTER_KEY_SIZE]; SECURE_RANDOM.nextBytes(decryptedKey); File keyFile = mock(File.class); @@ -221,7 +222,7 @@ void testConfigureSuccess() throws Exception { void testConfigureFallbackToCbc() throws Exception { char[] password = "password".toCharArray(); byte[] encryptedKey = new byte[]{1, 2, 3}; - byte[] decryptedKey = new byte[CryptoManager.KEY_SIZE]; + byte[] decryptedKey = new byte[MASTER_KEY_SIZE]; SECURE_RANDOM.nextBytes(decryptedKey); File keyFile = mock(File.class); @@ -322,7 +323,7 @@ void testVerifyKeyWithFileHash() throws Exception { void testDestroySecrets() throws Exception { char[] password = "password".toCharArray(); byte[] encryptedKey = new byte[]{1, 2, 3}; - byte[] decryptedKey = new byte[CryptoManager.KEY_SIZE]; + byte[] decryptedKey = new byte[MASTER_KEY_SIZE]; File keyFile = mock(File.class); when(filesUtils.getInternalFile(null, "key.encrypted")).thenReturn(keyFile);