Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down
42 changes: 42 additions & 0 deletions core/src/main/java/app/notesr/core/security/dto/CryptoSecrets.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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).
*/
Expand All @@ -38,6 +43,43 @@ public void destroy() {
Arrays.fill(password, '\0');
}

/**
* Validates the integrity of the cryptographic secrets.
* <p>
* 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.
*
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/java/app/notesr/core/util/CharUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
17 changes: 16 additions & 1 deletion core/src/main/java/app/notesr/core/util/KeyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
78 changes: 78 additions & 0 deletions core/src/test/java/app/notesr/core/util/CharUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}
Loading
Loading