mirror of https://github.com/halo-dev/halo
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
parent
f3c3c91ca4
commit
de85156067
|
@ -28,13 +28,11 @@ import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.AnonymousUserConst;
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.security.DefaultUserDetailService;
|
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.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.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.PatAuthenticationManager;
|
||||||
import run.halo.app.security.authentication.pat.PatJwkSupplier;
|
|
||||||
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
|
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
|
||||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthorizationManager;
|
import run.halo.app.security.authentication.twofactor.TwoFactorAuthorizationManager;
|
||||||
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
||||||
|
@ -58,7 +56,7 @@ public class WebServerSecurityConfig {
|
||||||
ObjectProvider<SecurityConfigurer> securityConfigurers,
|
ObjectProvider<SecurityConfigurer> securityConfigurers,
|
||||||
ServerSecurityContextRepository securityContextRepository,
|
ServerSecurityContextRepository securityContextRepository,
|
||||||
ReactiveExtensionClient client,
|
ReactiveExtensionClient client,
|
||||||
PatJwkSupplier patJwkSupplier,
|
CryptoService cryptoService,
|
||||||
HaloProperties haloProperties) {
|
HaloProperties haloProperties) {
|
||||||
|
|
||||||
http.securityMatcher(pathMatchers("/**"))
|
http.securityMatcher(pathMatchers("/**"))
|
||||||
|
@ -85,7 +83,7 @@ public class WebServerSecurityConfig {
|
||||||
.oauth2ResourceServer(oauth2 -> {
|
.oauth2ResourceServer(oauth2 -> {
|
||||||
var authManagerResolver = builder().add(
|
var authManagerResolver = builder().add(
|
||||||
new PatServerWebExchangeMatcher(),
|
new PatServerWebExchangeMatcher(),
|
||||||
new PatAuthenticationManager(client, patJwkSupplier)
|
new PatAuthenticationManager(client, cryptoService)
|
||||||
)
|
)
|
||||||
// TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager.
|
// TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager.
|
||||||
.build();
|
.build();
|
||||||
|
@ -149,8 +147,4 @@ public class WebServerSecurityConfig {
|
||||||
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
|
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
RsaKeyScheduledGenerator rsaKeyScheduledGenerator(CryptoService cryptoService) {
|
|
||||||
return new RsaKeyScheduledGenerator(cryptoService);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
public interface CryptoService {
|
public interface CryptoService {
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates key pair.
|
|
||||||
*/
|
|
||||||
Mono<Void> generateKeys();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypts message with Base64 format.
|
* Decrypts message with Base64 format.
|
||||||
*
|
*
|
||||||
|
@ -24,4 +20,18 @@ public interface CryptoService {
|
||||||
*/
|
*/
|
||||||
Mono<byte[]> readPublicKey();
|
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();
|
||||||
|
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.exception.RateLimitExceededException;
|
import run.halo.app.infra.exception.RateLimitExceededException;
|
||||||
import run.halo.app.infra.utils.IpAddressUtils;
|
import run.halo.app.infra.utils.IpAddressUtils;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class LoginAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
|
public class LoginAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.springdoc.core.fn.builders.apiresponse.Builder;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
|
|
||||||
public class PublicKeyRouteBuilder {
|
public class PublicKeyRouteBuilder {
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,6 +27,7 @@ import reactor.util.retry.Retry;
|
||||||
import run.halo.app.extension.ExtensionUtil;
|
import run.halo.app.extension.ExtensionUtil;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.security.PersonalAccessToken;
|
import run.halo.app.security.PersonalAccessToken;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
|
|
||||||
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
|
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
|
||||||
|
@ -40,16 +41,19 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
private final CryptoService cryptoService;
|
||||||
|
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
|
|
||||||
public PatAuthenticationManager(ReactiveExtensionClient client, PatJwkSupplier jwkSupplier) {
|
public PatAuthenticationManager(ReactiveExtensionClient client, CryptoService cryptoService) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.delegate = getDelegate(jwkSupplier);
|
this.cryptoService = cryptoService;
|
||||||
|
this.delegate = getDelegate();
|
||||||
this.clock = Clock.systemDefaultZone();
|
this.clock = Clock.systemDefaultZone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReactiveAuthenticationManager getDelegate(PatJwkSupplier jwkSupplier) {
|
private ReactiveAuthenticationManager getDelegate() {
|
||||||
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(jwkSupplier.getJwk()))
|
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(cryptoService.getJwk()))
|
||||||
.build();
|
.build();
|
||||||
return new JwtReactiveAuthenticationManager(jwtDecoder);
|
return new JwtReactiveAuthenticationManager(jwtDecoder);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package run.halo.app.security.authentication.pat;
|
|
||||||
|
|
||||||
import com.nimbusds.jose.jwk.JWK;
|
|
||||||
|
|
||||||
public interface PatJwkSupplier {
|
|
||||||
|
|
||||||
JWK getJwk();
|
|
||||||
|
|
||||||
}
|
|
|
@ -39,7 +39,7 @@ import run.halo.app.infra.ExternalUrlSupplier;
|
||||||
import run.halo.app.infra.exception.AccessDeniedException;
|
import run.halo.app.infra.exception.AccessDeniedException;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
import run.halo.app.security.PersonalAccessToken;
|
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.authentication.pat.UserScopedPatHandler;
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
|
|
||||||
|
@ -66,14 +66,14 @@ public class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
|
|
||||||
public UserScopedPatHandlerImpl(ReactiveExtensionClient client,
|
public UserScopedPatHandlerImpl(ReactiveExtensionClient client,
|
||||||
PatJwkSupplier jwkSupplier,
|
CryptoService cryptoService,
|
||||||
ExternalUrlSupplier externalUrl,
|
ExternalUrlSupplier externalUrl,
|
||||||
RoleService roleService) {
|
RoleService roleService) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.externalUrl = externalUrl;
|
this.externalUrl = externalUrl;
|
||||||
this.roleService = roleService;
|
this.roleService = roleService;
|
||||||
|
|
||||||
var patJwk = jwkSupplier.getJwk();
|
var patJwk = cryptoService.getJwk();
|
||||||
var jwkSet = new ImmutableJWKSet<>(new JWKSet(patJwk));
|
var jwkSet = new ImmutableJWKSet<>(new JWKSet(patJwk));
|
||||||
this.patEncoder = new NimbusJwtEncoder(jwkSet);
|
this.patEncoder = new NimbusJwtEncoder(jwkSet);
|
||||||
this.keyId = patJwk.getKeyID();
|
this.keyId = patJwk.getKeyID();
|
||||||
|
|
|
@ -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.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -14,6 +20,7 @@ import java.security.interfaces.RSAPublicKey;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Set;
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
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.extension.ExtendWith;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import reactor.core.Exceptions;
|
import reactor.core.Exceptions;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.security.authentication.login.InvalidEncryptedMessageException;
|
import run.halo.app.security.authentication.login.InvalidEncryptedMessageException;
|
||||||
|
@ -36,18 +44,16 @@ class RsaKeyServiceTest {
|
||||||
Path tempDir;
|
Path tempDir;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() throws JOSEException {
|
||||||
service = new RsaKeyService(tempDir);
|
service = new RsaKeyService(tempDir);
|
||||||
|
service.afterPropertiesSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGenerateKeyPair()
|
void shouldGenerateKeyPair()
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
StepVerifier.create(service.generateKeys())
|
byte[] privKeyBytes = Files.readAllBytes(tempDir.resolve("pat_id_rsa"));
|
||||||
.verifyComplete();
|
byte[] pubKeyBytes = Files.readAllBytes(tempDir.resolve("pat_id_rsa.pub"));
|
||||||
// check the file
|
|
||||||
byte[] privKeyBytes = Files.readAllBytes(tempDir.resolve("id_rsa"));
|
|
||||||
byte[] pubKeyBytes = Files.readAllBytes(tempDir.resolve("id_rsa.pub"));
|
|
||||||
|
|
||||||
var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);
|
var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);
|
||||||
var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
|
var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
|
||||||
|
@ -60,10 +66,7 @@ class RsaKeyServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReadPublicKey() throws IOException {
|
void shouldReadPublicKey() throws IOException {
|
||||||
StepVerifier.create(service.generateKeys())
|
var realPubKeyBytes = Files.readAllBytes(tempDir.resolve("pat_id_rsa.pub"));
|
||||||
.verifyComplete();
|
|
||||||
|
|
||||||
var realPubKeyBytes = Files.readAllBytes(tempDir.resolve("id_rsa.pub"));
|
|
||||||
|
|
||||||
StepVerifier.create(service.readPublicKey())
|
StepVerifier.create(service.readPublicKey())
|
||||||
.assertNext(bytes -> assertArrayEquals(realPubKeyBytes, bytes))
|
.assertNext(bytes -> assertArrayEquals(realPubKeyBytes, bytes))
|
||||||
|
@ -72,9 +75,6 @@ class RsaKeyServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDecryptMessageCorrectly() {
|
void shouldDecryptMessageCorrectly() {
|
||||||
StepVerifier.create(service.generateKeys())
|
|
||||||
.verifyComplete();
|
|
||||||
|
|
||||||
final String message = "halo";
|
final String message = "halo";
|
||||||
|
|
||||||
var mono = service.readPublicKey()
|
var mono = service.readPublicKey()
|
||||||
|
@ -83,7 +83,7 @@ class RsaKeyServiceTest {
|
||||||
try {
|
try {
|
||||||
var keyFactory = KeyFactory.getInstance(RsaKeyService.ALGORITHM);
|
var keyFactory = KeyFactory.getInstance(RsaKeyService.ALGORITHM);
|
||||||
var pubKey = keyFactory.generatePublic(pubKeySpec);
|
var pubKey = keyFactory.generatePublic(pubKeySpec);
|
||||||
var cipher = Cipher.getInstance(RsaKeyService.ALGORITHM);
|
var cipher = Cipher.getInstance(RsaKeyService.TRANSFORMATION);
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
|
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
|
||||||
return cipher.doFinal(message.getBytes());
|
return cipher.doFinal(message.getBytes());
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException
|
} catch (NoSuchAlgorithmException | InvalidKeySpecException
|
||||||
|
@ -102,10 +102,21 @@ class RsaKeyServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFailToDecryptMessage() {
|
void shouldFailToDecryptMessage() {
|
||||||
StepVerifier.create(service.generateKeys())
|
|
||||||
.verifyComplete();
|
|
||||||
|
|
||||||
StepVerifier.create(service.decrypt("invalid-bytes".getBytes()))
|
StepVerifier.create(service.decrypt("invalid-bytes".getBytes()))
|
||||||
.verifyError(InvalidEncryptedMessageException.class);
|
.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());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -28,6 +28,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.infra.exception.RateLimitExceededException;
|
import run.halo.app.infra.exception.RateLimitExceededException;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class LoginAuthenticationConverterTest {
|
class LoginAuthenticationConverterTest {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class PublicKeyRouteBuilderTest {
|
class PublicKeyRouteBuilderTest {
|
||||||
|
|
Loading…
Reference in New Issue