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.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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.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);
|
||||
}
|
||||
|
|
|
@ -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.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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue