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 @@ -88,8 +88,6 @@ public class FirstTimeWizardNewToadlet extends WebTemplateToadlet {

private static final String UNEXPECTED_ERROR_MESSAGE = "Should not happen, please report! {}";

private boolean isPasswordAlreadySet;

FirstTimeWizardNewToadlet(HighLevelSimpleClient client, NodeClientCore core, Config config) {
super(client);
this.core = core;
Expand Down Expand Up @@ -123,12 +121,7 @@ public void handleMethodGET(URI uri, HTTPRequest request, ToadletContext ctx)
return;
}

// if the threat level is high, the password must already be set: user is running the wizard
// again?
isPasswordAlreadySet =
core.getNode().services().securityLevels().getPhysicalThreatLevel()
== SecurityLevels.PHYSICAL_THREAT_LEVEL.HIGH;
showForm(ctx, new FormModel().toModel());
showForm(ctx, new FormModel(isPasswordAlreadySet()).toModel());
}

/**
Expand Down Expand Up @@ -158,11 +151,12 @@ public void handleMethodPOST(URI uri, HTTPRequest request, ToadletContext ctx)
return;
}

FormModel formModel = new FormModel(request);
FormModel formModel = new FormModel(request, isPasswordAlreadySet());

if (formModel.isValid()) {
formModel.save();
super.writeTemporaryRedirect(ctx, "Wizard complete", WelcomeToadlet.ROOT_PATH);
return;
}

// form model not valid
Expand Down Expand Up @@ -205,7 +199,19 @@ private static String l10n(String key, String value) {
return NodeL10n.getBase().getString(L10N_PREFIX + key, value);
}

/**
* Returns whether physical security policy currently indicates an already-configured password.
*
* <p>In the wizard flow, HIGH physical threat implies the user has already completed password
* setup and should not be prompted to set it again.
*/
private boolean isPasswordAlreadySet() {
return core.getNode().services().securityLevels().getPhysicalThreatLevel()
== SecurityLevels.PHYSICAL_THREAT_LEVEL.HIGH;
}

private final class FormModel {
private final boolean passwordAlreadySet;

private String knowSomeone = "";

Expand Down Expand Up @@ -234,7 +240,8 @@ private final class FormModel {

private final Map<String, String> errors = new HashMap<>();

FormModel() {
FormModel(boolean passwordAlreadySet) {
this.passwordAlreadySet = passwordAlreadySet;
float storage = 100;
Option<Long> sizeOption =
network.crypta.config.Config.longOption(config.get("node"), "storeSize");
Expand Down Expand Up @@ -267,7 +274,8 @@ private final class FormModel {
}
}

FormModel(HTTPRequest request) {
FormModel(HTTPRequest request, boolean passwordAlreadySet) {
this.passwordAlreadySet = passwordAlreadySet;
knowSomeone = request.getPartAsStringFailsafe("knowSomeone", 20);
connectToStrangers = request.getPartAsStringFailsafe("connectToStrangers", 20);
haveMonthlyLimit = request.getPartAsStringFailsafe("haveMonthlyLimit", 20);
Expand Down Expand Up @@ -432,10 +440,10 @@ private Map<String, Object> toModel() {
model.put("minBandwidthMonthlyLimit", "%.2f".formatted(BandwidthLimit.MIN_MONTHLY_LIMIT));
model.put("storageLimit", storageLimit);
model.put("minStorageLimit", minStorageLimit);
if (!isPasswordAlreadySet) {
if (!passwordAlreadySet) {
model.put("setPassword", !setPassword.isEmpty() ? CHECKED_VALUE : "");
}
model.put("isPasswordAlreadySet", isPasswordAlreadySet);
model.put("isPasswordAlreadySet", passwordAlreadySet);

if (downloadLimitDetected == null || uploadLimitDetected == null) {
detectBandwidthLimit();
Expand Down Expand Up @@ -495,21 +503,23 @@ private void save() {

DatastoreSize.setDatastoreSize(storageLimit + "GiB", config);

if (!isPasswordAlreadySet) {
if (!passwordAlreadySet) {
try {
String newPassword;
if (setPassword.isEmpty()) { // no password protection requested
core.getNode()
.services()
.securityLevels()
.setThreatLevel(SecurityLevels.PHYSICAL_THREAT_LEVEL.NORMAL);
core.getNode().storage().setMasterPassword("", true);
newPassword = "";
} else {
core.getNode()
.services()
.securityLevels()
.setThreatLevel(SecurityLevels.PHYSICAL_THREAT_LEVEL.HIGH);
core.getNode().storage().setMasterPassword(password, true);
newPassword = password;
}
core.getNode().storage().changeMasterPassword("", newPassword, true);
} catch (Node.AlreadySetPasswordException
| MasterKeysWrongPasswordException
| MasterKeysFileSizeException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,27 @@ public BookmarkManager(NodeClientCore n, boolean publicGateway) {
this.bookmarksFile = n.getNode().userDir().file("bookmarks.dat");
this.backupBookmarksFile = n.getNode().userDir().file("bookmarks.dat.bak");

try {
// Read the backup file if necessary
if (!bookmarksFile.exists() || bookmarksFile.length() == 0) throw new IOException();
LOG.info("Attempting to read the bookmark file from {}", bookmarksFile);
SimpleFieldSet sfs = SimpleFieldSet.readFrom(bookmarksFile, false, true);
readBookmarks(MAIN_CATEGORY, sfs);
} catch (MalformedURLException _) {
// Bookmark file contains a malformed key; ignore and fall back to the backup/defaults.
} catch (IOException ioe) {
LOG.error("Error reading the bookmark file ({}):{}", bookmarksFile, ioe.getMessage(), ioe);
boolean loadedPrimary = false;
if (!bookmarksFile.exists() || bookmarksFile.length() == 0) {
LOG.info(
"Bookmark file {} is missing or empty; loading backup/default bookmarks", bookmarksFile);
} else {
try {
LOG.info("Attempting to read the bookmark file from {}", bookmarksFile);
SimpleFieldSet sfs = SimpleFieldSet.readFrom(bookmarksFile, false, true);
readBookmarks(MAIN_CATEGORY, sfs);
loadedPrimary = true;
} catch (MalformedURLException e) {
LOG.warn(
"Bookmark file {} contains malformed keys; falling back to backup/default bookmarks",
bookmarksFile,
e);
} catch (IOException ioe) {
LOG.error("Error reading the bookmark file ({}):{}", bookmarksFile, ioe.getMessage(), ioe);
}
}

if (!loadedPrimary) {
try {
if (backupBookmarksFile.exists()
&& backupBookmarksFile.canRead()
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/network/crypta/node/UptimeEstimator.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public void start() {
}

private void readData(File file, int base) {
if (!file.exists()) {
LOG.debug("Uptime history file not found at startup: {}", file);
return;
}
try (FileInputStream fis = new FileInputStream(file);
DataInputStream dis = new DataInputStream(fis)) {
while (true) {
Expand All @@ -127,7 +131,9 @@ private void readData(File file, int base) {
}
}
} catch (EOFException _) {
// Reached end of file; no more samples to load.
// Reached the end of the file; no more samples to load.
} catch (FileNotFoundException _) {
LOG.debug("Uptime history file disappeared before it could be read: {}", file);
} catch (IOException _) {
LOG.error("Read old uptime file failed: {}; treating slots as offline", file);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicReference;
import network.crypta.clients.http.PasswordFormOptions;
import network.crypta.config.InvalidConfigValueException;
Expand Down Expand Up @@ -2169,9 +2170,22 @@ public void changeMasterPassword(
}
if (node.services().securityLevels().getPhysicalThreatLevel() == PHYSICAL_THREAT_LEVEL.MAXIMUM)
LOG.error("Changing password while physical threat level is at MAXIMUM???");
SecureRandom secureRandom = node.bootstrap().secureRandom();
if (masterKeysFile.exists()) {
keys.changePassword(masterKeysFile, newPassword, node.bootstrap().secureRandom());
setPasswordInner(keys, inFirstTimeWizard);
MasterKeys activeKeys = keys;
if (activeKeys == null) {
activeKeys = MasterKeys.read(masterKeysFile, secureRandom, oldPassword);
synchronized (node) {
if (keys == null) {
keys = activeKeys;
databaseKey = activeKeys.createDatabaseKey();
} else {
activeKeys = keys;
}
}
}
activeKeys.changePassword(masterKeysFile, newPassword, secureRandom);
setPasswordInner(activeKeys, inFirstTimeWizard);
} else {
setMasterPassword(newPassword, inFirstTimeWizard);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ void handleMethodPOST_whenValidInput_savesConfigAndRedirects() throws Exception
verify(fproxySubConfig).set("hasCompletedWizard", true);
verify(securityLevels).setThreatLevel(SecurityLevels.NETWORK_THREAT_LEVEL.NORMAL);
verify(securityLevels).setThreatLevel(SecurityLevels.PHYSICAL_THREAT_LEVEL.NORMAL);
verify(storage).setMasterPassword("", true);
verify(storage).changeMasterPassword("", "", true);
verify(core).storeConfig();
verify(ctx)
.sendReplyHeaders(
Expand All @@ -241,10 +241,8 @@ void handleMethodPOST_whenValidInput_savesConfigAndRedirects() throws Exception
ArgumentMatchers.any(),
eq("text/html; charset=UTF-8"),
anyLong());
assertTrue(toadlet.htmlWritten);
@SuppressWarnings("unchecked")
Map<String, String> errors = (Map<String, String>) toadlet.lastModel.get("errors");
assertTrue(errors.isEmpty());
assertFalse(toadlet.htmlWritten);
assertNull(toadlet.lastModel);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import java.io.File;
import java.lang.reflect.Field;
import java.security.SecureRandom;
import network.crypta.config.InvalidConfigValueException;
import network.crypta.config.NodeNeedRestartException;
import network.crypta.keys.CHKBlock;
import network.crypta.node.DatabaseKey;
import network.crypta.node.MasterKeys;
import network.crypta.node.Node;
import network.crypta.node.NodeClientCore;
import network.crypta.node.NodeInitException;
import network.crypta.node.ProgramDirectory;
import network.crypta.node.SecurityLevels;
import network.crypta.node.useralerts.UserAlert;
import network.crypta.node.useralerts.UserAlertManager;
import network.crypta.store.CHKStore;
Expand All @@ -20,6 +24,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Expand All @@ -29,6 +34,7 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -237,6 +243,43 @@ void setStorePreallocate_whenStoreIsWrapped_appliesToUnderlyingSaltedHashStore()
verify(saltedHashStore).setPreallocate(true);
}

@Test
void changeMasterPassword_whenKeysUnavailableAndMasterKeysFileExists_loadsKeysAndChangesPassword()
throws Exception {
File masterKeysFile = File.createTempFile("master-keys", ".tmp");
masterKeysFile.deleteOnExit();
subsystem.setMasterKeysFile(masterKeysFile);

MasterKeys loadedKeys = org.mockito.Mockito.mock(MasterKeys.class);
DatabaseKey loadedDatabaseKey = org.mockito.Mockito.mock(DatabaseKey.class);
SecurityLevels securityLevels = org.mockito.Mockito.mock(SecurityLevels.class);
NodeBootstrap bootstrap = org.mockito.Mockito.mock(NodeBootstrap.class);
SecureRandom secureRandom = new SecureRandom();

when(node.services()).thenReturn(services);
when(services.securityLevels()).thenReturn(securityLevels);
when(services.clientCore()).thenReturn(clientCore);
when(securityLevels.getPhysicalThreatLevel())
.thenReturn(SecurityLevels.PHYSICAL_THREAT_LEVEL.NORMAL);
when(node.bootstrap()).thenReturn(bootstrap);
when(bootstrap.secureRandom()).thenReturn(secureRandom);
when(loadedKeys.createDatabaseKey()).thenReturn(loadedDatabaseKey);

try (MockedStatic<MasterKeys> masterKeysStatic = mockStatic(MasterKeys.class)) {
masterKeysStatic
.when(() -> MasterKeys.read(masterKeysFile, secureRandom, "old-password"))
.thenReturn(loadedKeys);

subsystem.changeMasterPassword("old-password", "new-password", true);

masterKeysStatic.verify(() -> MasterKeys.read(masterKeysFile, secureRandom, "old-password"));
}

verify(loadedKeys).changePassword(masterKeysFile, "new-password", secureRandom);
assertSame(loadedKeys, subsystem.getKeys());
assertSame(loadedDatabaseKey, subsystem.getDatabaseKey());
}

private static void setChkDatastore(NodeStorageSubsystem target, CHKStore value)
throws ReflectiveOperationException {
Field field = target.getClass().getDeclaredField("chkDatastore");
Expand Down
Loading