| 1 | package edu.ucsb.cs156.frontiers.services; | |
| 2 | ||
| 3 | import edu.ucsb.cs156.frontiers.entities.ApiCourseKey; | |
| 4 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 5 | import edu.ucsb.cs156.frontiers.entities.User; | |
| 6 | import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException; | |
| 7 | import edu.ucsb.cs156.frontiers.repositories.ApiCourseKeyRepository; | |
| 8 | import edu.ucsb.cs156.frontiers.repositories.CourseRepository; | |
| 9 | import jakarta.servlet.http.HttpServletRequest; | |
| 10 | import java.nio.charset.StandardCharsets; | |
| 11 | import java.security.MessageDigest; | |
| 12 | import java.security.NoSuchAlgorithmException; | |
| 13 | import java.security.SecureRandom; | |
| 14 | import java.time.ZonedDateTime; | |
| 15 | import java.util.Base64; | |
| 16 | import java.util.List; | |
| 17 | import java.util.Optional; | |
| 18 | import org.springframework.stereotype.Service; | |
| 19 | import org.springframework.web.context.request.RequestAttributes; | |
| 20 | import org.springframework.web.context.request.RequestContextHolder; | |
| 21 | import org.springframework.web.context.request.ServletRequestAttributes; | |
| 22 | ||
| 23 | @Service | |
| 24 | public class ApiCourseKeyService { | |
| 25 | public enum ExpirationChoice { | |
| 26 | DAYS_90, | |
| 27 | MONTHS_6 | |
| 28 | } | |
| 29 | ||
| 30 | public static final String API_KEY_HEADER = "X-API-KEY"; | |
| 31 | ||
| 32 | public record GeneratedApiCourseKey(String rawKey, ApiCourseKey apiCourseKey) {} | |
| 33 | ||
| 34 | private final ApiCourseKeyRepository apiCourseKeyRepository; | |
| 35 | private final CourseRepository courseRepository; | |
| 36 | private final SecureRandom secureRandom = new SecureRandom(); | |
| 37 | ||
| 38 | public ApiCourseKeyService( | |
| 39 | ApiCourseKeyRepository apiCourseKeyRepository, CourseRepository courseRepository) { | |
| 40 | this.apiCourseKeyRepository = apiCourseKeyRepository; | |
| 41 | this.courseRepository = courseRepository; | |
| 42 | } | |
| 43 | ||
| 44 | public GeneratedApiCourseKey createKey( | |
| 45 | Long courseId, User creator, ExpirationChoice expirationChoice) { | |
| 46 | Course course = | |
| 47 | courseRepository | |
| 48 | .findById(courseId) | |
| 49 |
1
1. lambda$createKey$0 : replaced return value with null for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::lambda$createKey$0 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 50 | ||
| 51 | String rawKey = generateRawKey(); | |
| 52 | String salt = randomHex(16); | |
| 53 | ZonedDateTime now = ZonedDateTime.now(); | |
| 54 | ZonedDateTime expiresAt = | |
| 55 |
1
1. createKey : negated conditional → KILLED |
expirationChoice == ExpirationChoice.MONTHS_6 ? now.plusMonths(6) : now.plusDays(90); |
| 56 | ||
| 57 | ApiCourseKey apiCourseKey = | |
| 58 | ApiCourseKey.builder() | |
| 59 | .course(course) | |
| 60 | .createdBy(creator) | |
| 61 | .keyHash(hashWithSalt(rawKey, salt)) | |
| 62 | .salt(salt) | |
| 63 |
1
1. createKey : Replaced integer subtraction with addition → KILLED |
.keySuffix(rawKey.substring(Math.max(0, rawKey.length() - 6))) |
| 64 | .createdAt(now) | |
| 65 | .expiresAt(expiresAt) | |
| 66 | .build(); | |
| 67 | ||
| 68 | ApiCourseKey saved = apiCourseKeyRepository.save(apiCourseKey); | |
| 69 |
1
1. createKey : replaced return value with null for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::createKey → KILLED |
return new GeneratedApiCourseKey(rawKey, saved); |
| 70 | } | |
| 71 | ||
| 72 | public List<ApiCourseKey> listActiveKeys(Long courseId) { | |
| 73 |
1
1. listActiveKeys : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::listActiveKeys → KILLED |
return apiCourseKeyRepository |
| 74 | .findByCourseIdAndRevokedFalseAndExpiresAtAfterOrderByCreatedAtDesc( | |
| 75 | courseId, ZonedDateTime.now()); | |
| 76 | } | |
| 77 | ||
| 78 | public ApiCourseKey revokeKey(Long courseId, Long apiKeyId) { | |
| 79 | ApiCourseKey key = | |
| 80 | apiCourseKeyRepository | |
| 81 | .findByIdAndCourseId(apiKeyId, courseId) | |
| 82 |
1
1. lambda$revokeKey$1 : replaced return value with null for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::lambda$revokeKey$1 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(ApiCourseKey.class, apiKeyId)); |
| 83 |
1
1. revokeKey : removed call to edu/ucsb/cs156/frontiers/entities/ApiCourseKey::setRevoked → KILLED |
key.setRevoked(true); |
| 84 |
1
1. revokeKey : replaced return value with null for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::revokeKey → KILLED |
return apiCourseKeyRepository.save(key); |
| 85 | } | |
| 86 | ||
| 87 | public boolean authenticateFromRequestForCourse(Long courseId) { | |
| 88 | RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); | |
| 89 |
1
1. authenticateFromRequestForCourse : negated conditional → KILLED |
if (!(requestAttributes instanceof ServletRequestAttributes servletRequestAttributes)) { |
| 90 |
1
1. authenticateFromRequestForCourse : replaced boolean return with true for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::authenticateFromRequestForCourse → KILLED |
return false; |
| 91 | } | |
| 92 | HttpServletRequest request = servletRequestAttributes.getRequest(); | |
| 93 | String rawKey = request.getHeader(API_KEY_HEADER); | |
| 94 |
2
1. authenticateFromRequestForCourse : replaced boolean return with true for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::authenticateFromRequestForCourse → KILLED 2. authenticateFromRequestForCourse : replaced boolean return with false for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::authenticateFromRequestForCourse → KILLED |
return authenticateRawKeyForCourse(rawKey, courseId).isPresent(); |
| 95 | } | |
| 96 | ||
| 97 | public Optional<ApiCourseKey> authenticateRawKeyForCourse(String rawKey, Long courseId) { | |
| 98 |
2
1. authenticateRawKeyForCourse : negated conditional → KILLED 2. authenticateRawKeyForCourse : negated conditional → KILLED |
if (rawKey == null || rawKey.isBlank()) { |
| 99 | return Optional.empty(); | |
| 100 | } | |
| 101 | ||
| 102 | List<ApiCourseKey> activeKeys = listActiveKeys(courseId); | |
| 103 | for (ApiCourseKey key : activeKeys) { | |
| 104 |
1
1. authenticateRawKeyForCourse : negated conditional → KILLED |
if (hashMatches(rawKey, key)) { |
| 105 |
2
1. authenticateRawKeyForCourse : Replaced long addition with subtraction → KILLED 2. authenticateRawKeyForCourse : removed call to edu/ucsb/cs156/frontiers/entities/ApiCourseKey::setUsageCount → KILLED |
key.setUsageCount(key.getUsageCount() + 1); |
| 106 |
1
1. authenticateRawKeyForCourse : removed call to edu/ucsb/cs156/frontiers/entities/ApiCourseKey::setLastUsedAt → KILLED |
key.setLastUsedAt(ZonedDateTime.now()); |
| 107 |
1
1. authenticateRawKeyForCourse : replaced return value with Optional.empty for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::authenticateRawKeyForCourse → KILLED |
return Optional.of(apiCourseKeyRepository.save(key)); |
| 108 | } | |
| 109 | } | |
| 110 | return Optional.empty(); | |
| 111 | } | |
| 112 | ||
| 113 | private String generateRawKey() { | |
| 114 | byte[] raw = new byte[32]; | |
| 115 |
1
1. generateRawKey : removed call to java/security/SecureRandom::nextBytes → KILLED |
secureRandom.nextBytes(raw); |
| 116 |
1
1. generateRawKey : replaced return value with "" for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::generateRawKey → KILLED |
return "frt_" + Base64.getUrlEncoder().withoutPadding().encodeToString(raw); |
| 117 | } | |
| 118 | ||
| 119 | private String randomHex(int bytes) { | |
| 120 | byte[] raw = new byte[bytes]; | |
| 121 |
1
1. randomHex : removed call to java/security/SecureRandom::nextBytes → KILLED |
secureRandom.nextBytes(raw); |
| 122 | StringBuilder sb = new StringBuilder(); | |
| 123 | for (byte b : raw) { | |
| 124 | sb.append(String.format("%02x", b)); | |
| 125 | } | |
| 126 |
1
1. randomHex : replaced return value with "" for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::randomHex → KILLED |
return sb.toString(); |
| 127 | } | |
| 128 | ||
| 129 | private String hashWithSalt(String rawKey, String salt) { | |
| 130 |
1
1. hashWithSalt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::hashWithSalt → KILLED |
return Base64.getEncoder().encodeToString(hashBytes(rawKey, salt)); |
| 131 | } | |
| 132 | ||
| 133 | private byte[] hashBytes(String rawKey, String salt) { | |
| 134 | try { | |
| 135 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); | |
| 136 |
1
1. hashBytes : replaced return value with null for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::hashBytes → KILLED |
return digest.digest((salt + rawKey).getBytes(StandardCharsets.UTF_8)); |
| 137 | } catch (NoSuchAlgorithmException e) { | |
| 138 | throw new IllegalStateException("SHA-256 unavailable", e); | |
| 139 | } | |
| 140 | } | |
| 141 | ||
| 142 | private boolean hashMatches(String rawKey, ApiCourseKey key) { | |
| 143 | byte[] candidate = hashBytes(rawKey, key.getSalt()); | |
| 144 | try { | |
| 145 | byte[] expected = Base64.getDecoder().decode(key.getKeyHash()); | |
| 146 |
2
1. hashMatches : replaced boolean return with false for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::hashMatches → KILLED 2. hashMatches : replaced boolean return with true for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::hashMatches → KILLED |
return MessageDigest.isEqual(candidate, expected); |
| 147 | } catch (IllegalArgumentException e) { | |
| 148 |
1
1. hashMatches : replaced boolean return with true for edu/ucsb/cs156/frontiers/services/ApiCourseKeyService::hashMatches → KILLED |
return false; |
| 149 | } | |
| 150 | } | |
| 151 | } | |
Mutations | ||
| 49 |
1.1 |
|
| 55 |
1.1 |
|
| 63 |
1.1 |
|
| 69 |
1.1 |
|
| 73 |
1.1 |
|
| 82 |
1.1 |
|
| 83 |
1.1 |
|
| 84 |
1.1 |
|
| 89 |
1.1 |
|
| 90 |
1.1 |
|
| 94 |
1.1 2.2 |
|
| 98 |
1.1 2.2 |
|
| 104 |
1.1 |
|
| 105 |
1.1 2.2 |
|
| 106 |
1.1 |
|
| 107 |
1.1 |
|
| 115 |
1.1 |
|
| 116 |
1.1 |
|
| 121 |
1.1 |
|
| 126 |
1.1 |
|
| 130 |
1.1 |
|
| 136 |
1.1 |
|
| 146 |
1.1 2.2 |
|
| 148 |
1.1 |