Refactor CryptoService for simplifying RSA key generation (#5978)

#### What type of PR is this?

/kind cleanup
/area core
/milestone 2.16.x

#### What this PR does / why we need it:

This PR removes PatJwkSupplier interface, scheduled RSA key generation, and move some of them into CryptoService.

Currently, we only use `pat_id_rsa` as private key for authentication modules instead of `id_rsa`(deprecated).

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/5929/head
John Niang 2024-05-24 12:32:50 +08:00 committed by GitHub
parent f3c3c91ca4
commit de85156067
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 227 additions and 305 deletions

View File

@ -28,13 +28,11 @@ import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.security.DefaultUserDetailService;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authentication.SecurityConfigurer;
import run.halo.app.security.authentication.login.CryptoService;
import run.halo.app.security.authentication.impl.RsaKeyService;
import run.halo.app.security.authentication.login.PublicKeyRouteBuilder;
import run.halo.app.security.authentication.login.RsaKeyScheduledGenerator;
import run.halo.app.security.authentication.login.impl.RsaKeyService;
import run.halo.app.security.authentication.pat.PatAuthenticationManager;
import run.halo.app.security.authentication.pat.PatJwkSupplier;
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
import run.halo.app.security.authentication.twofactor.TwoFactorAuthorizationManager;
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
@ -58,7 +56,7 @@ public class WebServerSecurityConfig {
ObjectProvider<SecurityConfigurer> securityConfigurers,
ServerSecurityContextRepository securityContextRepository,
ReactiveExtensionClient client,
PatJwkSupplier patJwkSupplier,
CryptoService cryptoService,
HaloProperties haloProperties) {
http.securityMatcher(pathMatchers("/**"))
@ -85,7 +83,7 @@ public class WebServerSecurityConfig {
.oauth2ResourceServer(oauth2 -> {
var authManagerResolver = builder().add(
new PatServerWebExchangeMatcher(),
new PatAuthenticationManager(client, patJwkSupplier)
new PatAuthenticationManager(client, cryptoService)
)
// TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager.
.build();
@ -149,8 +147,4 @@ public class WebServerSecurityConfig {
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
}
@Bean
RsaKeyScheduledGenerator rsaKeyScheduledGenerator(CryptoService cryptoService) {
return new RsaKeyScheduledGenerator(cryptoService);
}
}

View File

@ -1,14 +1,10 @@
package run.halo.app.security.authentication.login;
package run.halo.app.security.authentication;
import com.nimbusds.jose.jwk.JWK;
import reactor.core.publisher.Mono;
public interface CryptoService {
/**
* Generates key pair.
*/
Mono<Void> generateKeys();
/**
* Decrypts message with Base64 format.
*
@ -24,4 +20,18 @@ public interface CryptoService {
*/
Mono<byte[]> readPublicKey();
/**
* Gets key ID of private key.
*
* @return key ID of private key.
*/
String getKeyId();
/**
* Gets JSON Web Keys.
*
* @return JSON Web Keys
*/
JWK getJwk();
}

View File

@ -0,0 +1,162 @@
package run.halo.app.security.authentication.impl;
import static com.nimbusds.jose.jwk.KeyOperation.SIGN;
import static com.nimbusds.jose.jwk.KeyOperation.VERIFY;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.codec.Hex;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authentication.login.InvalidEncryptedMessageException;
@Slf4j
public class RsaKeyService implements CryptoService, InitializingBean {
public static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding";
public static final String ALGORITHM = "RSA";
private final Path keysRoot;
private KeyPair keyPair;
private String keyId;
private JWK jwk;
public RsaKeyService(Path dir) {
this.keysRoot = dir;
}
@Override
public void afterPropertiesSet() throws JOSEException {
this.keyPair = this.getRsaKeyPairOrCreate();
this.keyId = sha256(keyPair.getPrivate().getEncoded());
this.jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey(keyPair.getPrivate())
.keyUse(KeyUse.SIGNATURE)
.keyOperations(Set.of(SIGN, VERIFY))
.keyIDFromThumbprint()
.algorithm(JWSAlgorithm.RS256)
.build();
}
private KeyPair getRsaKeyPairOrCreate() {
var privKeyPath = keysRoot.resolve("pat_id_rsa");
var pubKeyPath = keysRoot.resolve("pat_id_rsa.pub");
try {
if (Files.exists(privKeyPath) && Files.exists(pubKeyPath)) {
log.debug("Skip initializing RSA Keys for PAT due to existence.");
var keyFactory = KeyFactory.getInstance(ALGORITHM);
var privKeyBytes = Files.readAllBytes(privKeyPath);
var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
var privKey = keyFactory.generatePrivate(privKeySpec);
var pubKeyBytes = Files.readAllBytes(pubKeyPath);
var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);
var pubKey = keyFactory.generatePublic(pubKeySpec);
return new KeyPair(pubKey, privKey);
}
if (Files.notExists(keysRoot)) {
Files.createDirectories(keysRoot);
}
Files.createFile(privKeyPath);
Files.createFile(pubKeyPath);
log.info("Generating RSA keys for PAT.");
var rsaKey = new RSAKeyGenerator(4096).generate();
var pubKey = rsaKey.toRSAPublicKey();
var privKey = rsaKey.toRSAPrivateKey();
Files.write(privKeyPath, privKey.getEncoded(), TRUNCATE_EXISTING);
Files.write(pubKeyPath, pubKey.getEncoded(), TRUNCATE_EXISTING);
log.info("Wrote RSA keys for PAT into {} and {}", privKeyPath, pubKeyPath);
return new KeyPair(pubKey, privKey);
} catch (JOSEException | IOException
| InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to generate or read RSA key pair", e);
}
}
@Override
public Mono<byte[]> decrypt(byte[] encryptedMessage) {
return Mono.just(this.keyPair)
.map(KeyPair::getPrivate)
.flatMap(privateKey -> {
try {
var cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return Mono.just(cipher.doFinal(encryptedMessage));
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidKeyException e) {
return Mono.error(new RuntimeException(
"Failed to read private key or the key was invalid.", e
));
} catch (IllegalBlockSizeException | BadPaddingException e) {
return Mono.error(new InvalidEncryptedMessageException(
"Invalid encrypted message."
));
}
})
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public Mono<byte[]> readPublicKey() {
return Mono.just(keyPair)
.map(KeyPair::getPublic)
.map(PublicKey::getEncoded);
}
@Override
public String getKeyId() {
return this.keyId;
}
@Override
public JWK getJwk() {
return this.jwk;
}
private static String sha256(byte[] data) {
try {
var md = MessageDigest.getInstance("SHA-256");
return new String(Hex.encode(md.digest(data)));
} catch (NoSuchAlgorithmException e) {
// should never happen
throw new RuntimeException("Cannot obtain SHA-256 algorithm for message digest.", e);
}
}
}

View File

@ -15,6 +15,7 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.infra.exception.RateLimitExceededException;
import run.halo.app.infra.utils.IpAddressUtils;
import run.halo.app.security.authentication.CryptoService;
@Slf4j
public class LoginAuthenticationConverter extends ServerFormLoginAuthenticationConverter {

View File

@ -18,6 +18,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authentication.SecurityConfigurer;
@Component

View File

@ -6,6 +6,7 @@ import org.springdoc.core.fn.builders.apiresponse.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.security.authentication.CryptoService;
public class PublicKeyRouteBuilder {

View File

@ -1,25 +0,0 @@
package run.halo.app.security.authentication.login;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
/**
* RsaKeyScheduledGenerator is responsible for periodically generating RSA key pair.
*
* @author johnniang
*/
@Slf4j
public class RsaKeyScheduledGenerator {
private final CryptoService cryptoService;
public RsaKeyScheduledGenerator(CryptoService cryptoService) {
this.cryptoService = cryptoService;
}
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS)
void scheduleGeneration() {
cryptoService.generateKeys().block();
}
}

View File

@ -1,141 +0,0 @@
package run.halo.app.security.authentication.login.impl;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.util.StopWatch;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.security.authentication.login.CryptoService;
import run.halo.app.security.authentication.login.InvalidEncryptedMessageException;
@Slf4j
public class RsaKeyService implements CryptoService {
public static final String ALGORITHM = "RSA";
private final Path privateKeyPath;
private final Path publicKeyPath;
public RsaKeyService(Path dir) {
privateKeyPath = dir.resolve("id_rsa");
publicKeyPath = dir.resolve("id_rsa.pub");
}
@Override
public Mono<Void> generateKeys() {
try {
log.info("Generating RSA keys...");
var stopWatch = new StopWatch("GenerateRSAKeys");
stopWatch.start();
var generator = KeyPairGenerator.getInstance(ALGORITHM);
generator.initialize(2048);
var keyPair = generator.generateKeyPair();
stopWatch.stop();
log.info("Generated RSA keys. Usage: {} ms.", stopWatch.getTotalTimeMillis());
var dataBufferFactory = DefaultDataBufferFactory.sharedInstance;
var privateKeyDataBuffer = Mono.<DataBuffer>fromSupplier(() ->
dataBufferFactory.wrap(keyPair.getPrivate().getEncoded()));
var publicKeyDataBuffer = Mono.<DataBuffer>fromSupplier(() ->
dataBufferFactory.wrap(keyPair.getPublic().getEncoded()));
var writePrivateKey =
DataBufferUtils.write(privateKeyDataBuffer, privateKeyPath, TRUNCATE_EXISTING);
var writePublicKey =
DataBufferUtils.write(publicKeyDataBuffer, publicKeyPath, TRUNCATE_EXISTING);
return Mono.when(
createFileIfNotExist(privateKeyPath),
createFileIfNotExist(publicKeyPath))
.then(Mono.when(
writePrivateKey,
writePublicKey));
} catch (NoSuchAlgorithmException e) {
return Mono.error(e);
}
}
@Override
public Mono<byte[]> decrypt(byte[] encryptedMessage) {
return readKey(privateKeyPath)
.map(privateKeyBytes -> {
var keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
try {
var keyFactory = KeyFactory.getInstance(ALGORITHM);
var privateKey = keyFactory.generatePrivate(keySpec);
var cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedMessage);
} catch (NoSuchAlgorithmException | InvalidKeySpecException
| NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException("Failed to read private key or the key was invalid.",
e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
// invalid encrypted message
throw new InvalidEncryptedMessageException("Invalid encrypted message.", e);
}
});
}
@Override
public Mono<byte[]> readPublicKey() {
return readKey(publicKeyPath);
}
private Mono<byte[]> readKey(Path keyPath) {
var content =
DataBufferUtils.read(keyPath, DefaultDataBufferFactory.sharedInstance, 4096);
return DataBufferUtils.join(content)
.map(dataBuffer -> {
// the byte count won't be too large
var byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount());
dataBuffer.toByteBuffer(byteBuffer);
return byteBuffer.array();
});
}
Mono<Void> createFileIfNotExist(Path path) {
if (Files.notExists(path)) {
return Mono.fromRunnable(() -> {
try {
Files.createDirectories(path.getParent());
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
Files.createFile(path,
PosixFilePermissions.asFileAttribute(Set.of(OWNER_READ, OWNER_WRITE)));
} else {
Files.createFile(path);
}
} catch (IOException e) {
// ignore the error
log.warn("Failed to create file for {}", path, e);
}
}).subscribeOn(Schedulers.boundedElastic()).then();
}
return Mono.empty();
}
}

View File

@ -1,89 +0,0 @@
package run.halo.app.security.authentication.pat;
import static com.nimbusds.jose.jwk.KeyOperation.SIGN;
import static com.nimbusds.jose.jwk.KeyOperation.VERIFY;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import run.halo.app.infra.properties.HaloProperties;
@Slf4j
@Component
public class DefaultPatJwkSupplier implements PatJwkSupplier {
private final RSAKey rsaKey;
public DefaultPatJwkSupplier(HaloProperties haloProperties) throws JOSEException {
var keyPair = getRsaKeyPairOrCreate(haloProperties);
this.rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey(keyPair.getPrivate())
.keyUse(KeyUse.SIGNATURE)
.keyOperations(Set.of(SIGN, VERIFY))
.keyIDFromThumbprint()
.algorithm(JWSAlgorithm.RS256)
.build();
}
private KeyPair getRsaKeyPairOrCreate(HaloProperties haloProperties) {
var keysRoot = haloProperties.getWorkDir().resolve("keys");
var privKeyPath = keysRoot.resolve("pat_id_rsa");
var pubKeyPath = keysRoot.resolve("pat_id_rsa.pub");
try {
if (Files.exists(privKeyPath) && Files.exists(pubKeyPath)) {
log.debug("Skip initializing RSA Keys for PAT due to existence.");
var keyFactory = KeyFactory.getInstance("RSA");
var privKeyBytes = Files.readAllBytes(privKeyPath);
var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
var privKey = keyFactory.generatePrivate(privKeySpec);
var pubKeyBytes = Files.readAllBytes(pubKeyPath);
var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);
var pubKey = keyFactory.generatePublic(pubKeySpec);
return new KeyPair(pubKey, privKey);
}
if (Files.notExists(keysRoot)) {
Files.createDirectories(keysRoot);
}
Files.createFile(privKeyPath);
Files.createFile(pubKeyPath);
log.info("Generating RSA keys for PAT.");
var rsaKey = new RSAKeyGenerator(4096).generate();
var pubKey = rsaKey.toRSAPublicKey();
var privKey = rsaKey.toRSAPrivateKey();
Files.write(privKeyPath, privKey.getEncoded(), TRUNCATE_EXISTING);
Files.write(pubKeyPath, pubKey.getEncoded(), TRUNCATE_EXISTING);
log.info("Wrote RSA keys for PAT into {} and {}", privKeyPath, pubKeyPath);
return new KeyPair(pubKey, privKey);
} catch (JOSEException | IOException
| InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public JWK getJwk() {
return rsaKey;
}
}

View File

@ -27,6 +27,7 @@ import reactor.util.retry.Retry;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.security.PersonalAccessToken;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authorization.AuthorityUtils;
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
@ -40,16 +41,19 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
private final ReactiveExtensionClient client;
private final CryptoService cryptoService;
private Clock clock;
public PatAuthenticationManager(ReactiveExtensionClient client, PatJwkSupplier jwkSupplier) {
public PatAuthenticationManager(ReactiveExtensionClient client, CryptoService cryptoService) {
this.client = client;
this.delegate = getDelegate(jwkSupplier);
this.cryptoService = cryptoService;
this.delegate = getDelegate();
this.clock = Clock.systemDefaultZone();
}
private static ReactiveAuthenticationManager getDelegate(PatJwkSupplier jwkSupplier) {
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(jwkSupplier.getJwk()))
private ReactiveAuthenticationManager getDelegate() {
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(cryptoService.getJwk()))
.build();
return new JwtReactiveAuthenticationManager(jwtDecoder);
}

View File

@ -1,9 +0,0 @@
package run.halo.app.security.authentication.pat;
import com.nimbusds.jose.jwk.JWK;
public interface PatJwkSupplier {
JWK getJwk();
}

View File

@ -39,7 +39,7 @@ import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.AccessDeniedException;
import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.security.PersonalAccessToken;
import run.halo.app.security.authentication.pat.PatJwkSupplier;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authentication.pat.UserScopedPatHandler;
import run.halo.app.security.authorization.AuthorityUtils;
@ -66,14 +66,14 @@ public class UserScopedPatHandlerImpl implements UserScopedPatHandler {
private Clock clock;
public UserScopedPatHandlerImpl(ReactiveExtensionClient client,
PatJwkSupplier jwkSupplier,
CryptoService cryptoService,
ExternalUrlSupplier externalUrl,
RoleService roleService) {
this.client = client;
this.externalUrl = externalUrl;
this.roleService = roleService;
var patJwk = jwkSupplier.getJwk();
var patJwk = cryptoService.getJwk();
var jwkSet = new ImmutableJWKSet<>(new JWKSet(patJwk));
this.patEncoder = new NimbusJwtEncoder(jwkSet);
this.keyId = patJwk.getKeyID();

View File

@ -1,8 +1,14 @@
package run.halo.app.security.authentication.login.impl;
package run.halo.app.security.authentication.impl;
import static com.nimbusds.jose.jwk.KeyOperation.SIGN;
import static com.nimbusds.jose.jwk.KeyOperation.VERIFY;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.KeyUse;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -14,6 +20,7 @@ import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
@ -23,6 +30,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.util.StringUtils;
import reactor.core.Exceptions;
import reactor.test.StepVerifier;
import run.halo.app.security.authentication.login.InvalidEncryptedMessageException;
@ -36,18 +44,16 @@ class RsaKeyServiceTest {
Path tempDir;
@BeforeEach
void setUp() {
void setUp() throws JOSEException {
service = new RsaKeyService(tempDir);
service.afterPropertiesSet();
}
@Test
void shouldGenerateKeyPair()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
StepVerifier.create(service.generateKeys())
.verifyComplete();
// check the file
byte[] privKeyBytes = Files.readAllBytes(tempDir.resolve("id_rsa"));
byte[] pubKeyBytes = Files.readAllBytes(tempDir.resolve("id_rsa.pub"));
byte[] privKeyBytes = Files.readAllBytes(tempDir.resolve("pat_id_rsa"));
byte[] pubKeyBytes = Files.readAllBytes(tempDir.resolve("pat_id_rsa.pub"));
var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);
var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
@ -60,10 +66,7 @@ class RsaKeyServiceTest {
@Test
void shouldReadPublicKey() throws IOException {
StepVerifier.create(service.generateKeys())
.verifyComplete();
var realPubKeyBytes = Files.readAllBytes(tempDir.resolve("id_rsa.pub"));
var realPubKeyBytes = Files.readAllBytes(tempDir.resolve("pat_id_rsa.pub"));
StepVerifier.create(service.readPublicKey())
.assertNext(bytes -> assertArrayEquals(realPubKeyBytes, bytes))
@ -72,9 +75,6 @@ class RsaKeyServiceTest {
@Test
void shouldDecryptMessageCorrectly() {
StepVerifier.create(service.generateKeys())
.verifyComplete();
final String message = "halo";
var mono = service.readPublicKey()
@ -83,7 +83,7 @@ class RsaKeyServiceTest {
try {
var keyFactory = KeyFactory.getInstance(RsaKeyService.ALGORITHM);
var pubKey = keyFactory.generatePublic(pubKeySpec);
var cipher = Cipher.getInstance(RsaKeyService.ALGORITHM);
var cipher = Cipher.getInstance(RsaKeyService.TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return cipher.doFinal(message.getBytes());
} catch (NoSuchAlgorithmException | InvalidKeySpecException
@ -102,10 +102,21 @@ class RsaKeyServiceTest {
@Test
void shouldFailToDecryptMessage() {
StepVerifier.create(service.generateKeys())
.verifyComplete();
StepVerifier.create(service.decrypt("invalid-bytes".getBytes()))
.verifyError(InvalidEncryptedMessageException.class);
}
@Test
void shouldGetKeyIdFromJwk() {
assertTrue(StringUtils.hasText(service.getKeyId()));
}
@Test
void shouldGetJwk() {
var jwk = service.getJwk();
assertEquals("RSA", jwk.getKeyType().getValue());
assertEquals(JWSAlgorithm.RS256, jwk.getAlgorithm());
assertEquals(KeyUse.SIGNATURE, jwk.getKeyUse());
assertEquals(Set.of(SIGN, VERIFY), jwk.getKeyOperations());
}
}

View File

@ -28,6 +28,7 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.infra.exception.RateLimitExceededException;
import run.halo.app.security.authentication.CryptoService;
@ExtendWith(MockitoExtension.class)
class LoginAuthenticationConverterTest {

View File

@ -13,6 +13,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import run.halo.app.security.authentication.CryptoService;
@ExtendWith(MockitoExtension.class)
class PublicKeyRouteBuilderTest {