|
1
|
|
package edu.ucsb.cs156.frontiers.services; |
|
2
|
|
|
|
3
|
|
import java.nio.charset.StandardCharsets; |
|
4
|
|
import java.security.GeneralSecurityException; |
|
5
|
|
import java.security.SecureRandom; |
|
6
|
|
import java.util.Arrays; |
|
7
|
|
import java.util.Base64; |
|
8
|
|
import javax.crypto.Cipher; |
|
9
|
|
import javax.crypto.spec.GCMParameterSpec; |
|
10
|
|
import javax.crypto.spec.SecretKeySpec; |
|
11
|
|
import org.springframework.beans.factory.annotation.Value; |
|
12
|
|
import org.springframework.stereotype.Service; |
|
13
|
|
|
|
14
|
|
@Service |
|
15
|
|
public class CanvasApiTokenSecurityService { |
|
16
|
|
|
|
17
|
|
private static final String ENCRYPTION_PREFIX = "enc:v1:"; |
|
18
|
|
private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding"; |
|
19
|
|
private static final int GCM_TAG_LENGTH_BITS = 128; |
|
20
|
|
private static final int GCM_IV_LENGTH_BYTES = 12; |
|
21
|
|
|
|
22
|
|
private final String encodedEncryptionKey; |
|
23
|
|
private final SecureRandom secureRandom = new SecureRandom(); |
|
24
|
|
|
|
25
|
|
public CanvasApiTokenSecurityService( |
|
26
|
|
@Value("${app.canvas.api-token-encryption-key:}") String encodedEncryptionKey) { |
|
27
|
|
this.encodedEncryptionKey = encodedEncryptionKey; |
|
28
|
|
} |
|
29
|
|
|
|
30
|
|
public String encrypt(String canvasApiToken) { |
|
31
|
2
1. encrypt : negated conditional → KILLED
2. encrypt : negated conditional → KILLED
|
if (canvasApiToken == null || canvasApiToken.isEmpty()) { |
|
32
|
1
1. encrypt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::encrypt → KILLED
|
return canvasApiToken; |
|
33
|
|
} |
|
34
|
1
1. encrypt : negated conditional → KILLED
|
if (canvasApiToken.startsWith(ENCRYPTION_PREFIX)) { |
|
35
|
1
1. encrypt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::encrypt → KILLED
|
return canvasApiToken; |
|
36
|
|
} |
|
37
|
|
|
|
38
|
|
try { |
|
39
|
|
Cipher cipher = getCipher(); |
|
40
|
|
byte[] iv = new byte[GCM_IV_LENGTH_BYTES]; |
|
41
|
1
1. encrypt : removed call to java/security/SecureRandom::nextBytes → KILLED
|
secureRandom.nextBytes(iv); |
|
42
|
1
1. encrypt : removed call to javax/crypto/Cipher::init → KILLED
|
cipher.init( |
|
43
|
|
Cipher.ENCRYPT_MODE, getSecretKey(), new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)); |
|
44
|
|
|
|
45
|
|
byte[] ciphertext = cipher.doFinal(canvasApiToken.getBytes(StandardCharsets.UTF_8)); |
|
46
|
1
1. encrypt : Replaced integer addition with subtraction → KILLED
|
byte[] payload = new byte[iv.length + ciphertext.length]; |
|
47
|
1
1. encrypt : removed call to java/lang/System::arraycopy → KILLED
|
System.arraycopy(iv, 0, payload, 0, iv.length); |
|
48
|
1
1. encrypt : removed call to java/lang/System::arraycopy → KILLED
|
System.arraycopy(ciphertext, 0, payload, iv.length, ciphertext.length); |
|
49
|
1
1. encrypt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::encrypt → KILLED
|
return ENCRYPTION_PREFIX + Base64.getEncoder().encodeToString(payload); |
|
50
|
|
} catch (GeneralSecurityException e) { |
|
51
|
|
throw new IllegalStateException("Failed to encrypt Canvas API token", e); |
|
52
|
|
} |
|
53
|
|
} |
|
54
|
|
|
|
55
|
|
public String decrypt(String storedCanvasApiToken) { |
|
56
|
2
1. decrypt : negated conditional → KILLED
2. decrypt : negated conditional → KILLED
|
if (storedCanvasApiToken == null || storedCanvasApiToken.isEmpty()) { |
|
57
|
1
1. decrypt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::decrypt → KILLED
|
return storedCanvasApiToken; |
|
58
|
|
} |
|
59
|
1
1. decrypt : negated conditional → KILLED
|
if (!storedCanvasApiToken.startsWith(ENCRYPTION_PREFIX)) { |
|
60
|
1
1. decrypt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::decrypt → KILLED
|
return storedCanvasApiToken; |
|
61
|
|
} |
|
62
|
|
|
|
63
|
|
try { |
|
64
|
|
byte[] payload = |
|
65
|
|
Base64.getDecoder().decode(storedCanvasApiToken.substring(ENCRYPTION_PREFIX.length())); |
|
66
|
2
1. decrypt : changed conditional boundary → KILLED
2. decrypt : negated conditional → KILLED
|
if (payload.length <= GCM_IV_LENGTH_BYTES) { |
|
67
|
|
throw new IllegalStateException("Encrypted Canvas API token is malformed"); |
|
68
|
|
} |
|
69
|
|
byte[] iv = Arrays.copyOfRange(payload, 0, GCM_IV_LENGTH_BYTES); |
|
70
|
|
byte[] ciphertext = Arrays.copyOfRange(payload, GCM_IV_LENGTH_BYTES, payload.length); |
|
71
|
|
|
|
72
|
|
Cipher cipher = getCipher(); |
|
73
|
1
1. decrypt : removed call to javax/crypto/Cipher::init → KILLED
|
cipher.init( |
|
74
|
|
Cipher.DECRYPT_MODE, getSecretKey(), new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)); |
|
75
|
|
byte[] plaintext = cipher.doFinal(ciphertext); |
|
76
|
1
1. decrypt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::decrypt → KILLED
|
return new String(plaintext, StandardCharsets.UTF_8); |
|
77
|
|
} catch (GeneralSecurityException | IllegalArgumentException e) { |
|
78
|
|
throw new IllegalStateException("Failed to decrypt Canvas API token", e); |
|
79
|
|
} |
|
80
|
|
} |
|
81
|
|
|
|
82
|
|
protected Cipher getCipher() throws GeneralSecurityException { |
|
83
|
1
1. getCipher : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::getCipher → KILLED
|
return Cipher.getInstance(CIPHER_ALGORITHM); |
|
84
|
|
} |
|
85
|
|
|
|
86
|
|
private SecretKeySpec getSecretKey() { |
|
87
|
2
1. getSecretKey : negated conditional → KILLED
2. getSecretKey : negated conditional → KILLED
|
if (encodedEncryptionKey == null || encodedEncryptionKey.isBlank()) { |
|
88
|
|
throw new IllegalStateException( |
|
89
|
|
"Canvas API token encryption key is not configured; use 'openssl rand -base64 32' to generate one."); |
|
90
|
|
} |
|
91
|
|
byte[] decodedKey; |
|
92
|
|
try { |
|
93
|
|
decodedKey = Base64.getDecoder().decode(encodedEncryptionKey); |
|
94
|
|
} catch (IllegalArgumentException e) { |
|
95
|
|
throw new IllegalStateException("Canvas API token encryption key must be Base64 encoded", e); |
|
96
|
|
} |
|
97
|
3
1. getSecretKey : negated conditional → KILLED
2. getSecretKey : negated conditional → KILLED
3. getSecretKey : negated conditional → KILLED
|
if (decodedKey.length != 16 && decodedKey.length != 24 && decodedKey.length != 32) { |
|
98
|
|
throw new IllegalStateException( |
|
99
|
|
"Canvas API token encryption key must decode to 16, 24, or 32 bytes"); |
|
100
|
|
} |
|
101
|
1
1. getSecretKey : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::getSecretKey → KILLED
|
return new SecretKeySpec(decodedKey, "AES"); |
|
102
|
|
} |
|
103
|
|
} |
| | Mutations |
| 31 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_whenCipherCreationFails_throwsIllegalStateException()] negated conditional → KILLED
2.2 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_whenCipherCreationFails_throwsIllegalStateException()] negated conditional → KILLED
|
| 32 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_nullOrEmptyOrAlreadyEncrypted_returnsInputUnchanged()] replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::encrypt → KILLED
|
| 34 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_whenCipherCreationFails_throwsIllegalStateException()] negated conditional → KILLED
|
| 35 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_nullOrEmptyOrAlreadyEncrypted_returnsInputUnchanged()] replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::encrypt → KILLED
|
| 41 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_samePlaintextTwice_usesDifferentRandomIv()] removed call to java/security/SecureRandom::nextBytes → KILLED
|
| 42 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_samePlaintextTwice_usesDifferentRandomIv()] removed call to javax/crypto/Cipher::init → KILLED
|
| 46 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_samePlaintextTwice_usesDifferentRandomIv()] Replaced integer addition with subtraction → KILLED
|
| 47 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_thenDecrypt_with24ByteKey_roundTripsToken()] removed call to java/lang/System::arraycopy → KILLED
|
| 48 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_thenDecrypt_with24ByteKey_roundTripsToken()] removed call to java/lang/System::arraycopy → KILLED
|
| 49 |
|
1.1 Location : encrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_samePlaintextTwice_usesDifferentRandomIv()] replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::encrypt → KILLED
|
| 56 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_ivOnlyPayload_throwsMalformedException()] negated conditional → KILLED
2.2 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_nullOrEmpty_returnsInputUnchanged()] negated conditional → KILLED
|
| 57 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_nullOrEmpty_returnsInputUnchanged()] replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::decrypt → KILLED
|
| 59 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_plaintextLegacyValue_returnsOriginalValue()] negated conditional → KILLED
|
| 60 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_plaintextLegacyValue_returnsOriginalValue()] replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::decrypt → KILLED
|
| 66 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_ivOnlyPayload_throwsMalformedException()] changed conditional boundary → KILLED
2.2 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:decrypt_ivOnlyPayload_throwsMalformedException()] negated conditional → KILLED
|
| 73 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_thenDecrypt_with24ByteKey_roundTripsToken()] removed call to javax/crypto/Cipher::init → KILLED
|
| 76 |
|
1.1 Location : decrypt Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_thenDecrypt_with24ByteKey_roundTripsToken()] replaced return value with "" for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::decrypt → KILLED
|
| 83 |
|
1.1 Location : getCipher Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_samePlaintextTwice_usesDifferentRandomIv()] replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::getCipher → KILLED
|
| 87 |
|
1.1 Location : getSecretKey Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_withNonBase64Key_throwsIllegalStateException()] negated conditional → KILLED
2.2 Location : getSecretKey Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_withoutConfiguredKey_throwsIllegalStateException()] negated conditional → KILLED
|
| 97 |
|
1.1 Location : getSecretKey Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_withInvalidDecodedKeyLength_throwsIllegalStateException()] negated conditional → KILLED
2.2 Location : getSecretKey Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_withInvalidDecodedKeyLength_throwsIllegalStateException()] negated conditional → KILLED
3.3 Location : getSecretKey Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_withInvalidDecodedKeyLength_throwsIllegalStateException()] negated conditional → KILLED
|
| 101 |
|
1.1 Location : getSecretKey Killed by : edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasApiTokenSecurityServiceTests]/[method:encrypt_samePlaintextTwice_usesDifferentRandomIv()] replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasApiTokenSecurityService::getSecretKey → KILLED
|