Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSCEXAM-1240 Examination event specific quit password #1026

Merged
merged 1 commit into from
Jan 12, 2024
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
73 changes: 53 additions & 20 deletions app/controllers/ExaminationEventController.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,13 @@ public Result insertExaminationEvent(Long eid, Http.Request request) {
if (capacity + ub > configReader.getMaxByodExaminationParticipantCount()) {
return forbidden("i18n_error_max_capacity_exceeded");
}
String password = request.attrs().get(Attrs.SETTINGS_PASSWORD);
if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH && password == null) {
return forbidden("no password provided");
String quitPassword = request.attrs().get(Attrs.QUIT_PASSWORD);
if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH && quitPassword == null) {
return forbidden("no quit password provided");
}
String settingsPassword = request.attrs().get(Attrs.SETTINGS_PASSWORD);
if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH && settingsPassword == null) {
return forbidden("no settings password provided");
}
ee.setStart(start);
ee.setDescription(request.attrs().get(Attrs.DESCRIPTION));
Expand All @@ -147,12 +151,14 @@ public Result insertExaminationEvent(Long eid, Http.Request request) {
eec.setExaminationEvent(ee);
eec.setExam(exam);
eec.setHash(UUID.randomUUID().toString());
if (password != null) {
encryptSettingsPassword(eec, password);
if (quitPassword != null && settingsPassword != null) {
encryptQuitPassword(eec, quitPassword);
encryptSettingsPassword(eec, settingsPassword, quitPassword);
// Pass back the plaintext password, so it can be shown to user
eec.setQuitPassword(quitPassword);
eec.setSettingsPassword(settingsPassword);
}
eec.save();
// Pass back the plaintext password, so it can be shown to user
eec.setSettingsPassword(request.attrs().get(Attrs.SETTINGS_PASSWORD));
return ok(eec);
}

Expand All @@ -172,9 +178,13 @@ public Result updateExaminationEvent(Long eid, Long eecid, Http.Request request)
ExaminationEventConfiguration eec = oeec.get();
boolean hasEnrolments = !eec.getExamEnrolments().isEmpty();
ExaminationEvent ee = eec.getExaminationEvent();
String password = request.attrs().get(Attrs.SETTINGS_PASSWORD);
if (eec.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH && password == null) {
return forbidden("no password provided");
String quitPassword = request.attrs().get(Attrs.QUIT_PASSWORD);
if (eec.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH && quitPassword == null) {
return forbidden("no quit password provided");
}
String settingsPassword = request.attrs().get(Attrs.SETTINGS_PASSWORD);
if (eec.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH && settingsPassword == null) {
return forbidden("no settings password provided");
}
DateTime start = request.attrs().get(Attrs.START_DATE);
if (!hasEnrolments) {
Expand All @@ -194,19 +204,23 @@ public Result updateExaminationEvent(Long eid, Long eecid, Http.Request request)
}
ee.setCapacity(capacity);
ee.setDescription(request.attrs().get(Attrs.DESCRIPTION));

ee.update();
if (password == null) {
if (quitPassword == null || settingsPassword == null) {
return ok(eec);
} else if (!hasEnrolments) {
encryptSettingsPassword(eec, password);
}
if (!hasEnrolments) {
encryptQuitPassword(eec, settingsPassword);
encryptSettingsPassword(eec, settingsPassword, quitPassword);
eec.save();
// Pass back the plaintext password, so it can be shown to user
eec.setSettingsPassword(request.attrs().get(Attrs.SETTINGS_PASSWORD));
// Pass back the plaintext passwords, so they can be shown to user
eec.setQuitPassword(quitPassword);
eec.setSettingsPassword(settingsPassword);
} else {
// Disallow changing password if enrolments exist for this event
// TODO: check how this could be made possible. Would need resending seb-files with new encryption
// Send back the original (unchanged password)
// Pass back the original unchanged passwords
eec.setQuitPassword(
byodConfigHandler.getPlaintextPassword(eec.getEncryptedQuitPassword(), eec.getQuitPasswordSalt())
);
eec.setSettingsPassword(
byodConfigHandler.getPlaintextPassword(
eec.getEncryptedSettingsPassword(),
Expand Down Expand Up @@ -246,7 +260,7 @@ public Result removeExaminationEvent(Long eid, Long eeid) {
return ok();
}

private void encryptSettingsPassword(ExaminationEventConfiguration eec, String password) {
private void encryptSettingsPassword(ExaminationEventConfiguration eec, String password, String quitPassword) {
try {
String oldPwd = eec.getEncryptedSettingsPassword() != null
? byodConfigHandler.getPlaintextPassword(
Expand All @@ -260,7 +274,26 @@ private void encryptSettingsPassword(ExaminationEventConfiguration eec, String p
eec.setEncryptedSettingsPassword(byodConfigHandler.getEncryptedPassword(password, newSalt));
eec.setSettingsPasswordSalt(newSalt);
// Pre-calculate config key, so we don't need to do it each time a check is needed
eec.setConfigKey(byodConfigHandler.calculateConfigKey(eec.getHash()));
eec.setConfigKey(byodConfigHandler.calculateConfigKey(eec.getHash(), quitPassword));
}
} catch (Exception e) {
logger.error("unable to set settings password", e);
throw new RuntimeException(e);
}
}

private void encryptQuitPassword(ExaminationEventConfiguration eec, String password) {
try {
String oldPwd = eec.getEncryptedQuitPassword() != null
? byodConfigHandler.getPlaintextPassword(eec.getEncryptedQuitPassword(), eec.getQuitPasswordSalt())
: null;

if (!password.equals(oldPwd)) {
String newSalt = UUID.randomUUID().toString();
eec.setEncryptedQuitPassword(byodConfigHandler.getEncryptedPassword(password, newSalt));
eec.setQuitPasswordSalt(newSalt);
// Pre-calculate config key, so we don't need to do it each time a check is needed
eec.setConfigKey(byodConfigHandler.calculateConfigKey(eec.getHash(), password));
}
} catch (Exception e) {
logger.error("unable to set settings password", e);
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/StudentActionsController.java
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,19 @@ public Result getExamConfigFile(Long enrolmentId, Http.Request request) {
String examName = oee.get().getExam().getName();
ExaminationEventConfiguration eec = oee.get().getExaminationEventConfiguration();
String fileName = examName.replace(" ", "-");
String quitPassword = byodConfigHandler.getPlaintextPassword(
eec.getEncryptedQuitPassword(),
eec.getQuitPasswordSalt()
);
File file;
try {
file = File.createTempFile(fileName, ".seb");
FileOutputStream fos = new FileOutputStream(file);
byte[] data = byodConfigHandler.getExamConfig(
eec.getHash(),
eec.getEncryptedSettingsPassword(),
eec.getSettingsPasswordSalt()
eec.getSettingsPasswordSalt(),
quitPassword
);
fos.write(data);
fos.close();
Expand Down
7 changes: 6 additions & 1 deletion app/impl/EmailComposerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@ public void composeExaminationEventNotification(User recipient, ExamEnrolment en

if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH) {
// Attach a SEB config file
String quitPassword = byodConfigHandler.getPlaintextPassword(
config.getEncryptedQuitPassword(),
config.getQuitPasswordSalt()
);
String fileName = exam.getName().replace(" ", "-");
File file;
try {
Expand All @@ -356,7 +360,8 @@ public void composeExaminationEventNotification(User recipient, ExamEnrolment en
byte[] data = byodConfigHandler.getExamConfig(
config.getHash(),
config.getEncryptedSettingsPassword(),
config.getSettingsPasswordSalt()
config.getSettingsPasswordSalt(),
quitPassword
);
fos.write(data);
fos.close();
Expand Down
34 changes: 34 additions & 0 deletions app/models/ExaminationEventConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ public class ExaminationEventConfiguration extends GeneratedIdentityModel {
@JsonIgnore
private byte[] encryptedSettingsPassword;

@Lob
@JsonIgnore
private byte[] encryptedQuitPassword;

@JsonIgnore
private String settingsPasswordSalt;

@JsonIgnore
private String quitPasswordSalt;

@JsonIgnore
private String configKey;

Expand All @@ -60,6 +67,9 @@ public class ExaminationEventConfiguration extends GeneratedIdentityModel {
@Transient
private String settingsPassword;

@Transient
private String quitPassword;

public Exam getExam() {
return exam;
}
Expand Down Expand Up @@ -92,10 +102,26 @@ public void setEncryptedSettingsPassword(byte[] encryptedSettingsPassword) {
this.encryptedSettingsPassword = encryptedSettingsPassword;
}

public byte[] getEncryptedQuitPassword() {
return encryptedQuitPassword;
}

public void setEncryptedQuitPassword(byte[] encryptedQuitPassword) {
this.encryptedQuitPassword = encryptedQuitPassword;
}

public String getSettingsPasswordSalt() {
return settingsPasswordSalt;
}

public String getQuitPasswordSalt() {
return quitPasswordSalt;
}

public void setQuitPasswordSalt(String quitPasswordSalt) {
this.quitPasswordSalt = quitPasswordSalt;
}

public void setSettingsPasswordSalt(String settingsPasswordSalt) {
this.settingsPasswordSalt = settingsPasswordSalt;
}
Expand Down Expand Up @@ -124,6 +150,14 @@ public void setSettingsPassword(String settingsPassword) {
this.settingsPassword = settingsPassword;
}

public String getQuitPassword() {
return quitPassword;
}

public void setQuitPassword(String quitPassword) {
this.quitPassword = quitPassword;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
1 change: 1 addition & 0 deletions app/sanitizers/Attrs.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public enum Attrs {
public static final TypedKey<String> COURSE_CODE = TypedKey.create("code");
public static final TypedKey<Boolean> ANONYMOUS = TypedKey.create("anonymous");
public static final TypedKey<String> SETTINGS_PASSWORD = TypedKey.create("settingsPassword");
public static final TypedKey<String> QUIT_PASSWORD = TypedKey.create("quitPassword");
public static final TypedKey<String> QUESTION_TEXT = TypedKey.create("question");
public static final TypedKey<String> ANSWER_INSTRUCTIONS = TypedKey.create("answerInstructions");
public static final TypedKey<String> EVALUATION_CRITERIA = TypedKey.create("evaluationCriteria");
Expand Down
6 changes: 4 additions & 2 deletions app/sanitizers/ExaminationEventSanitizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class ExaminationEventSanitizer extends BaseSanitizer {
protected Http.Request sanitize(Http.Request req, JsonNode body) throws SanitizingException {
if (body.has("config")) {
JsonNode configNode = body.get("config");
String pwd = configNode.path("settingsPassword").asText(null);
String settingsPassword = configNode.path("settingsPassword").asText(null);
String quitPassword = configNode.path("quitPassword").asText(null);
JsonNode eventNode = configNode.get("examinationEvent");
DateTime dateTime = DateTime.parse(eventNode.get("start").asText(), ISODateTimeFormat.dateTime());
String description = eventNode.get("description").asText();
Expand All @@ -35,7 +36,8 @@ protected Http.Request sanitize(Http.Request req, JsonNode body) throws Sanitizi
.addAttr(Attrs.START_DATE, dateTime)
.addAttr(Attrs.DESCRIPTION, description)
.addAttr(Attrs.CAPACITY, capacity)
.addAttr(Attrs.SETTINGS_PASSWORD, pwd);
.addAttr(Attrs.SETTINGS_PASSWORD, settingsPassword)
.addAttr(Attrs.QUIT_PASSWORD, quitPassword);
} else {
throw new SanitizingException("missing required data");
}
Expand Down
4 changes: 2 additions & 2 deletions app/util/config/ByodConfigHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import java.util.Optional
import play.mvc.{Http, Result}

trait ByodConfigHandler:
def getExamConfig(hash: String, pwd: Array[Byte], salt: String): Array[Byte]
def calculateConfigKey(hash: String): String
def getExamConfig(hash: String, pwd: Array[Byte], salt: String, quitPwd: String): Array[Byte]
def calculateConfigKey(hash: String, quitPwd: String): String
def getPlaintextPassword(pwd: Array[Byte], salt: String): String
def getEncryptedPassword(pwd: String, salt: String): Array[Byte]
def checkUserAgent(request: Http.RequestHeader, examConfigKey: String): Optional[Result]
13 changes: 6 additions & 7 deletions app/util/config/ByodConfigHandlerImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,13 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm
/* FIXME: have Apache provide us with X-Forwarded-Proto header so we can resolve this automatically */
private val protocol = URI.create(configReader.getHostName).toURL.getProtocol

private def getTemplate(hash: String): String =
private def getTemplate(hash: String, quitPwdPlain: String): String =
val path = s"${env.rootPath.getAbsolutePath}/conf/seb.template.plist"
val startUrl = s"${configReader.getHostName}?exam=$hash"
val quitLink = configReader.getQuitExaminationLink
val adminPwd = DigestUtils.sha256Hex(configReader.getExaminationAdminPassword)
val quitPwdPlain = configReader.getQuitPassword
val quitPwd = DigestUtils.sha256Hex(quitPwdPlain)
val allowQuitting = if (quitPwdPlain.isEmpty) "<false/>" else "<true/>"
val allowQuitting = if quitPwdPlain.isEmpty then "<false/>" else "<true/>"
val source = Source.fromFile(path)
val template = source.mkString
.replace(StartUrlPlaceholder, startUrl)
Expand Down Expand Up @@ -98,8 +97,8 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm
.sortBy(_._1.toLowerCase)
Some(JsObject(json))

override def getExamConfig(hash: String, pwd: Array[Byte], salt: String): Array[Byte] =
val template = getTemplate(hash)
override def getExamConfig(hash: String, pwd: Array[Byte], salt: String, quitPwd: String): Array[Byte] =
val template = getTemplate(hash, quitPwd)
val templateGz = compress(template.getBytes(StandardCharsets.UTF_8))
// Decrypt user defined setting password
val plaintextPwd = getPlaintextPassword(pwd, salt)
Expand Down Expand Up @@ -128,14 +127,14 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm
Some(Results.unauthorized("Wrong configuration key digest")).toJava
}

override def calculateConfigKey(hash: String): String =
override def calculateConfigKey(hash: String, quitPwd: String): String =
// Override the DTD setting. We need it with PLIST format and in order to integrate with SBT
val parser = XML.withSAXParser {
val factory = SAXParserFactory.newInstance()
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
factory.newSAXParser()
}
val plist: Node = parser.loadString(getTemplate(hash))
val plist: Node = parser.loadString(getTemplate(hash, quitPwd))
// Construct a Json-like structure out of .plist and create a digest over it
// See SEB documentation for details
dictToJson((plist \ "dict").head) match
Expand Down
1 change: 0 additions & 1 deletion app/util/config/ConfigReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public interface ConfigReader {
String getQuitExaminationLink();
String getExaminationAdminPassword();
String getSettingsPasswordEncryptionKey();
String getQuitPassword();
String getHomeOrganisationRef();
Integer getMaxByodExaminationParticipantCount();
String getCourseCodePrefix();
Expand Down
5 changes: 0 additions & 5 deletions app/util/config/ConfigReaderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,6 @@ public String getSettingsPasswordEncryptionKey() {
return config.getString("exam.exam.seb.settingsPwd.encryption.key");
}

@Override
public String getQuitPassword() {
return config.getString("exam.exam.seb.quitPwd");
}

@Override
public String getHomeOrganisationRef() {
return config.getString("exam.integration.iop.organisationRef");
Expand Down
Loading
Loading