From de85156067ffffb3097082c1fc69ee173e80f0ce Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 24 May 2024 12:32:50 +0800 Subject: [PATCH] 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 ``` --- .../app/config/WebServerSecurityConfig.java | 14 +- .../{login => }/CryptoService.java | 22 ++- .../authentication/impl/RsaKeyService.java | 162 ++++++++++++++++++ .../login/LoginAuthenticationConverter.java | 1 + .../login/LoginSecurityConfigurer.java | 1 + .../login/PublicKeyRouteBuilder.java | 1 + .../login/RsaKeyScheduledGenerator.java | 25 --- .../login/impl/RsaKeyService.java | 141 --------------- .../pat/DefaultPatJwkSupplier.java | 89 ---------- .../pat/PatAuthenticationManager.java | 12 +- .../authentication/pat/PatJwkSupplier.java | 9 - .../pat/impl/UserScopedPatHandlerImpl.java | 6 +- .../{login => }/impl/RsaKeyServiceTest.java | 47 +++-- .../LoginAuthenticationConverterTest.java | 1 + .../login/PublicKeyRouteBuilderTest.java | 1 + 15 files changed, 227 insertions(+), 305 deletions(-) rename application/src/main/java/run/halo/app/security/authentication/{login => }/CryptoService.java (61%) create mode 100644 application/src/main/java/run/halo/app/security/authentication/impl/RsaKeyService.java delete mode 100644 application/src/main/java/run/halo/app/security/authentication/login/RsaKeyScheduledGenerator.java delete mode 100644 application/src/main/java/run/halo/app/security/authentication/login/impl/RsaKeyService.java delete mode 100644 application/src/main/java/run/halo/app/security/authentication/pat/DefaultPatJwkSupplier.java delete mode 100644 application/src/main/java/run/halo/app/security/authentication/pat/PatJwkSupplier.java rename application/src/test/java/run/halo/app/security/authentication/{login => }/impl/RsaKeyServiceTest.java (78%) diff --git a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index 1c476a5b4..8055a2dc1 100644 --- a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -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 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); - } } diff --git a/application/src/main/java/run/halo/app/security/authentication/login/CryptoService.java b/application/src/main/java/run/halo/app/security/authentication/CryptoService.java similarity index 61% rename from application/src/main/java/run/halo/app/security/authentication/login/CryptoService.java rename to application/src/main/java/run/halo/app/security/authentication/CryptoService.java index 99207b5ed..0084fda11 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/CryptoService.java +++ b/application/src/main/java/run/halo/app/security/authentication/CryptoService.java @@ -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 generateKeys(); - /** * Decrypts message with Base64 format. * @@ -24,4 +20,18 @@ public interface CryptoService { */ Mono 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(); + } diff --git a/application/src/main/java/run/halo/app/security/authentication/impl/RsaKeyService.java b/application/src/main/java/run/halo/app/security/authentication/impl/RsaKeyService.java new file mode 100644 index 000000000..2b762350f --- /dev/null +++ b/application/src/main/java/run/halo/app/security/authentication/impl/RsaKeyService.java @@ -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 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 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); + } + } + +} diff --git a/application/src/main/java/run/halo/app/security/authentication/login/LoginAuthenticationConverter.java b/application/src/main/java/run/halo/app/security/authentication/login/LoginAuthenticationConverter.java index 45267236e..9a3bfb7e1 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/LoginAuthenticationConverter.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/LoginAuthenticationConverter.java @@ -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 { diff --git a/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java b/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java index a4105a31b..6d0007586 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java @@ -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 diff --git a/application/src/main/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilder.java b/application/src/main/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilder.java index ec6be59e1..14f2250fd 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilder.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilder.java @@ -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 { diff --git a/application/src/main/java/run/halo/app/security/authentication/login/RsaKeyScheduledGenerator.java b/application/src/main/java/run/halo/app/security/authentication/login/RsaKeyScheduledGenerator.java deleted file mode 100644 index 77221273f..000000000 --- a/application/src/main/java/run/halo/app/security/authentication/login/RsaKeyScheduledGenerator.java +++ /dev/null @@ -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(); - } -} diff --git a/application/src/main/java/run/halo/app/security/authentication/login/impl/RsaKeyService.java b/application/src/main/java/run/halo/app/security/authentication/login/impl/RsaKeyService.java deleted file mode 100644 index 46b525336..000000000 --- a/application/src/main/java/run/halo/app/security/authentication/login/impl/RsaKeyService.java +++ /dev/null @@ -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 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.fromSupplier(() -> - dataBufferFactory.wrap(keyPair.getPrivate().getEncoded())); - var publicKeyDataBuffer = Mono.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 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 readPublicKey() { - return readKey(publicKeyPath); - } - - private Mono 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 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(); - } -} diff --git a/application/src/main/java/run/halo/app/security/authentication/pat/DefaultPatJwkSupplier.java b/application/src/main/java/run/halo/app/security/authentication/pat/DefaultPatJwkSupplier.java deleted file mode 100644 index 8183174c6..000000000 --- a/application/src/main/java/run/halo/app/security/authentication/pat/DefaultPatJwkSupplier.java +++ /dev/null @@ -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; - } -} diff --git a/application/src/main/java/run/halo/app/security/authentication/pat/PatAuthenticationManager.java b/application/src/main/java/run/halo/app/security/authentication/pat/PatAuthenticationManager.java index 019caffda..9cd886003 100644 --- a/application/src/main/java/run/halo/app/security/authentication/pat/PatAuthenticationManager.java +++ b/application/src/main/java/run/halo/app/security/authentication/pat/PatAuthenticationManager.java @@ -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); } diff --git a/application/src/main/java/run/halo/app/security/authentication/pat/PatJwkSupplier.java b/application/src/main/java/run/halo/app/security/authentication/pat/PatJwkSupplier.java deleted file mode 100644 index b023329f2..000000000 --- a/application/src/main/java/run/halo/app/security/authentication/pat/PatJwkSupplier.java +++ /dev/null @@ -1,9 +0,0 @@ -package run.halo.app.security.authentication.pat; - -import com.nimbusds.jose.jwk.JWK; - -public interface PatJwkSupplier { - - JWK getJwk(); - -} diff --git a/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java b/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java index 2dc914bb4..b6a77a08b 100644 --- a/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java +++ b/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java @@ -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(); diff --git a/application/src/test/java/run/halo/app/security/authentication/login/impl/RsaKeyServiceTest.java b/application/src/test/java/run/halo/app/security/authentication/impl/RsaKeyServiceTest.java similarity index 78% rename from application/src/test/java/run/halo/app/security/authentication/login/impl/RsaKeyServiceTest.java rename to application/src/test/java/run/halo/app/security/authentication/impl/RsaKeyServiceTest.java index c79be3e11..8d799204a 100644 --- a/application/src/test/java/run/halo/app/security/authentication/login/impl/RsaKeyServiceTest.java +++ b/application/src/test/java/run/halo/app/security/authentication/impl/RsaKeyServiceTest.java @@ -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()); + } } \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/security/authentication/login/LoginAuthenticationConverterTest.java b/application/src/test/java/run/halo/app/security/authentication/login/LoginAuthenticationConverterTest.java index 358a20818..a40207b96 100644 --- a/application/src/test/java/run/halo/app/security/authentication/login/LoginAuthenticationConverterTest.java +++ b/application/src/test/java/run/halo/app/security/authentication/login/LoginAuthenticationConverterTest.java @@ -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 { diff --git a/application/src/test/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilderTest.java b/application/src/test/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilderTest.java index 8b970179f..bc7c861a9 100644 --- a/application/src/test/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilderTest.java +++ b/application/src/test/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilderTest.java @@ -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 {