CanvasApiTokenSecurityService.java

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

Active mutators

Tests examined


Report generated by PIT 1.17.0