Skip to content

Commit

Permalink
Merge branch 'dev' into CSCEXAM-1154
Browse files Browse the repository at this point in the history
  • Loading branch information
lupari authored Jan 12, 2024
2 parents d3442b3 + 6bf7da7 commit f0e2228
Show file tree
Hide file tree
Showing 23 changed files with 189 additions and 187 deletions.
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

0 comments on commit f0e2228

Please sign in to comment.