feat: Add security config for halo (#1838)

* feat: Add security config for halo

* chore: delete file

* feat: add security config intergration test case

* refactor: use EnableWebSecurity annotation

* refactor: patch prefix

* Update src/test/java/run/halo/app/integration/security/AuthenticationTest.java

Co-authored-by: John Niang <johnniang@fastmail.com>

* refactor: mvc path prefix config

Co-authored-by: John Niang <johnniang@fastmail.com>
pull/1841/head
guqing 2022-04-13 17:08:34 +08:00 committed by GitHub
parent e297b0efcf
commit b3c82396ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 554 additions and 0 deletions

View File

@ -48,6 +48,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-jetty"
@ -55,6 +56,8 @@ dependencies {
implementation "org.flywaydb:flyway-mysql"
compileOnly 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly "com.h2database:h2"

View File

@ -0,0 +1,27 @@
package run.halo.app.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import run.halo.app.Application;
/**
* Spring web mvc config.
*
* @author guqing
* @date 2022-04-12
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api/v1",
c -> HandlerTypePredicate.forAnnotation(RestController.class)
.and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName()))
.test(c)
);
}
}

View File

@ -0,0 +1,107 @@
package run.halo.app.config;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import java.io.IOException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler;
import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint;
import run.halo.app.infra.properties.JwtProperties;
/**
* @author guqing
* @date 2022-04-12
*/
@EnableWebSecurity
@EnableConfigurationProperties(JwtProperties.class)
public class WebSecurityConfig {
private final RSAPublicKey key;
private final RSAPrivateKey priv;
public WebSecurityConfig(JwtProperties jwtProperties) throws IOException {
this.key = jwtProperties.readPublicKey();
this.priv = jwtProperties.readPrivateKey();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/api/**", "/apis/**").authenticated()
)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults())
.sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler())
);
// @formatter:on
return http.build();
}
@Bean
UserDetailsService users() {
// @formatter:off
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password("{noop}password")
.authorities("app")
.build()
);
// @formatter:on
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withUsername("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

View File

@ -0,0 +1,19 @@
package run.halo.app.identity.authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author guqing
* @date 2022-04-12
*/
@RestController
@RequestMapping("/oauth")
public class OauthController {
@GetMapping("login")
public String login() {
return "hello";
}
}

View File

@ -0,0 +1,57 @@
package run.halo.app.identity.entrypoint;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.util.Assert;
import run.halo.app.infra.utils.HaloUtils;
/**
* @author guqing
* @date 2022-04-12
*/
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private String errorPage;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (response.isCommitted()) {
log.trace("Did not write to response since already committed");
return;
}
if (this.errorPage == null) {
log.debug("Responding with 403 status code");
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
return;
}
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
// Set the 403 status code.
response.setStatus(HttpStatus.FORBIDDEN.value());
// forward to error page.
if (log.isDebugEnabled()) {
log.debug("Forwarding to [{}] with status code 403", this.errorPage);
}
if (HaloUtils.isAjaxRequest(request)) {
response.getWriter().write("access denied!");
return;
}
request.getRequestDispatcher(this.errorPage).forward(request, response);
}
public void setErrorPage(String errorPage) {
Assert.isTrue(errorPage == null || errorPage.startsWith("/"),
"errorPage must begin with '/'");
this.errorPage = errorPage;
}
}

View File

@ -0,0 +1,31 @@
package run.halo.app.identity.entrypoint;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import run.halo.app.infra.utils.HaloUtils;
/**
* @author guqing
* @date 2022-04-12
*/
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.warn("Unauthorized error: {}", authException.getMessage());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
if (HaloUtils.isAjaxRequest(request)) {
response.getWriter().write(HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
}

View File

@ -0,0 +1,17 @@
package run.halo.app.infra.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author guqing
* @date 2022-04-12
*/
@ConfigurationProperties(prefix = "halo")
public class HaloProperties {
private final JwtProperties jwt = new JwtProperties();
public JwtProperties getJwt() {
return jwt;
}
}

View File

@ -0,0 +1,86 @@
package run.halo.app.infra.properties;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.io.Resource;
import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
/**
* @author guqing
* @date 2022-04-12
*/
@ConfigurationProperties(prefix = "halo.security.oauth2.jwt")
public class JwtProperties {
/**
* JSON Web Algorithm used for verifying the digital signatures.
*/
private String jwsAlgorithm = "RS256";
/**
* Location of the file containing the public key used to verify a JWT.
*/
private Resource publicKeyLocation;
private Resource privateKeyLocation;
public String getJwsAlgorithm() {
return this.jwsAlgorithm;
}
public void setJwsAlgorithm(String jwsAlgorithm) {
this.jwsAlgorithm = jwsAlgorithm;
}
public Resource getPublicKeyLocation() {
return this.publicKeyLocation;
}
public void setPublicKeyLocation(Resource publicKeyLocation) {
this.publicKeyLocation = publicKeyLocation;
}
public Resource getPrivateKeyLocation() {
return privateKeyLocation;
}
public void setPrivateKeyLocation(Resource privateKeyLocation) {
this.privateKeyLocation = privateKeyLocation;
}
public RSAPublicKey readPublicKey() throws IOException {
String key = "halo.security.oauth2.jwt.public-key-location";
Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null");
if (!this.publicKeyLocation.exists()) {
throw new InvalidConfigurationPropertyValueException(key, this.publicKeyLocation,
"Public key location does not exist");
}
try (InputStream inputStream = this.publicKeyLocation.getInputStream()) {
String source = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
return RsaKeyConverters.x509()
.convert(new ByteArrayInputStream(source.getBytes()));
}
}
public RSAPrivateKey readPrivateKey() throws IOException {
String key = "halo.security.oauth2.jwt.private-key-location";
Assert.notNull(this.privateKeyLocation, "PrivateKeyLocation must not be null");
if (!this.privateKeyLocation.exists()) {
throw new InvalidConfigurationPropertyValueException(key, this.privateKeyLocation,
"Private key location does not exist");
}
try (InputStream inputStream = this.privateKeyLocation.getInputStream()) {
String source = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
return RsaKeyConverters.pkcs8()
.convert(new ByteArrayInputStream(source.getBytes()));
}
}
}

View File

@ -0,0 +1,14 @@
package run.halo.app.infra.utils;
import jakarta.servlet.http.HttpServletRequest;
/**
* @author guqing
* @date 2022-04-12
*/
public class HaloUtils {
public static boolean isAjaxRequest(HttpServletRequest request) {
String requestedWith = request.getHeader("x-requested-with");
return requestedWith == null || requestedWith.equalsIgnoreCase("XMLHttpRequest");
}
}

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOjnDY1K1lrOrK
ETfKfDlVGVbPCiy+TDmTaXg4SWjdHUpXfqbXMkSX/j2dJ/ECqb/FtsvVxiSwRieG
3MWDKWlNRz0C0QKrsoDYbcvLf68uc7L5eKFZhu0AkXP4T5BIbdMXH8V0+5e+6R+n
eHahFhMyaiYoHVrPMrW2Jn9iWIXuNTDpg9VFhejN4jG1wQqIu1puKeGYPQvtfNO5
Ef5cQdEFCvFfuDQvNhLgI1f798qY6EVFfRo2S3LLCut3wfDzRZiUN4Kz8qYz42Zv
97GS1gW/lfcEsmBApov9xiIaUzUECN35XbZYMK5Y4gfhAseZ+tlj+YarEiPjAtL1
JPUehCmRAgMBAAECggEAKmaI+ammsoFtbO9d4X3gkvxxmmx/RM0G4KC84ekH0qPp
l85S10flVsIEydbiHWbVC/P7IbXb4Cd2g7OcA9GjYQ6nkoVvI+mvkz3uoKZkQofT
jGxbyrHswroY8Tb76jJJK60E7n+a5cCbE9ihmW2boTSzAncMJg5FyM9cRMbhL0Vz
h90/gE2U8awQ8Ug47BN1Dk/awxB9f5zVqI+LCGC0Py0/oQudjSaqPihydTsuqkhV
xNu3NMcL/POt9WxmYyJFDJRW3+EYraPumdUsIWw8p4JJDt1jkyNpSbjGhu8vzRYX
0QSo1pa3VrDY4guEMk4RdJsKJDqQPTvCTTgDYBzFlQKBgQDq98CRLTwqHSEWyVKN
0KRujhVAVEmLDvPxZ2tVaMM37RanCHYSfHLiYCD54rUv7BFWjQ+hfq3iHUpgrefN
KRS9e01mT0f24sAsWfhrFzrhlHaQStFgOw4uvwIDCfzrBeQQsqcAvWSjNr8CqSMX
UIGz9oB6EP39PT3QxT3oYf3ItwKBgQDhC6WN78+0sf2zlptQ7V02eWaRfePtQfmb
ow3c9aF8V7sSwDzjInqV5Bva4RyftYRTYZttBiANjGZ1pSNPi/2p7b+0hxJ1pPf8
6VcFDJBGLbFYNDWOux13KRJToMY0ckzSeBXgkWLVFSfESuoXzy+8bj5eMavJLg6L
2Ek6q6mH9wKBgBZmmE0+6sV5EXaCqwQqKAMCOLRxVLGVM1yIZ4s0+aeTSt2RyO/q
PWmnkH1CR9PRxbVirWLQGPO9pyGgcsD0ca2+25otZMb8xyVzTmOnS03GQadv+pYa
CzgZra9sfFhLr3qIDbPcWoPU7FDsnxPR8QufLJB2nkBOXl5Q753/+ZnxAoGBAI47
GisWwaNmSv3R1d/T5PGk0Jprgj5VUDh5WS2pYKKBoA49yT2UcP2C6cfwNnMJ+dPp
AJ5rHJ7zeV4pPKPtyig3xs2GALixxrnlj8X1Jsnz3v3sIV1QDVNedeK83ggPpVXv
54PC3z/k2vlIj6L0oyroUiqeIgBIR5FC5SVbkQ4JAoGBAOEGQkqw1xR3fd27J6/R
s9hOhItPnjExf5yqeg0nbZYIGd+6PiaVBBWUefZDDS79KUwTiqiHGP7iEVghJr9C
xJI9odzY8WQJ+Q9ZQy1VQfP5mkRUTTkABhykXfWsHckO7yP6c3kwNIOOki8QPrmY
3GKNb5HtQVpazCvrB5PFh65g
-----END PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzo5w2NStZazqyhE3ynw5
VRlWzwosvkw5k2l4OElo3R1KV36m1zJEl/49nSfxAqm/xbbL1cYksEYnhtzFgylp
TUc9AtECq7KA2G3Ly3+vLnOy+XihWYbtAJFz+E+QSG3TFx/FdPuXvukfp3h2oRYT
MmomKB1azzK1tiZ/YliF7jUw6YPVRYXozeIxtcEKiLtabinhmD0L7XzTuRH+XEHR
BQrxX7g0LzYS4CNX+/fKmOhFRX0aNktyywrrd8Hw80WYlDeCs/KmM+Nmb/exktYF
v5X3BLJgQKaL/cYiGlM1BAjd+V22WDCuWOIH4QLHmfrZY/mGqxIj4wLS9ST1HoQp
kQIDAQAB
-----END PUBLIC KEY-----

View File

@ -39,6 +39,12 @@ spring:
locations:
- classpath:db/migration/common
- classpath:db/migration/{vendor}
halo:
security:
oauth2:
jwt:
public-key-location: classpath:app.pub
private-key-location: classpath:app.key
logging:
level:
run.halo.app: DEBUG

View File

@ -0,0 +1,30 @@
package run.halo.app;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerTypePredicate;
import run.halo.app.identity.authentication.OauthController;
/**
* Test case for api path prefix predicate.
*
* @author guqing
* @date 2022-04-13
*/
public class PathPrefixPredicateTest {
@Test
public void prefixPredicate() {
boolean falseResult = HandlerTypePredicate.forAnnotation(RestController.class)
.and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName()))
.test(getClass());
assertThat(falseResult).isFalse();
boolean result = HandlerTypePredicate.forAnnotation(RestController.class)
.and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName()))
.test(OauthController.class);
assertThat(result).isTrue();
}
}

View File

@ -0,0 +1,83 @@
package run.halo.app.integration.security;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import jakarta.servlet.Filter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import run.halo.app.config.WebSecurityConfig;
/**
* @author guqing
* @date 2022-04-12
*/
@TestPropertySource(properties = {"halo.security.oauth2.jwt.public-key-location=classpath:app.pub",
"halo.security.oauth2.jwt.private-key-location=classpath:app.key"})
@WebMvcTest
@Import(WebSecurityConfig.class)
public class AuthenticationTest {
private MockMvc mockMvc;
@Autowired
SecurityFilterChain securityFilterChain;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(AnonymousAllowedController.class,
SecuredController.class)
.addFilters(securityFilterChain.getFilters().toArray(new Filter[] {}))
.build();
}
@Test
public void allowAccessTest() throws Exception {
mockMvc.perform(get("/anonymous"))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(content().string("Hello, now you see me."));
}
@Test
public void securedApiTest() throws Exception {
mockMvc.perform(get("/api/secured"))
.andDo(print())
.andExpect(status().is(HttpStatus.UNAUTHORIZED.value()))
.andExpect(content().string("Unauthorized"));
}
@Controller
@RequestMapping("/anonymous")
static class AnonymousAllowedController {
@GetMapping
@ResponseBody
public String hello() {
return "Hello, now you see me.";
}
}
@RestController
@RequestMapping("/api/secured")
static class SecuredController {
@GetMapping
public String hello() {
return "You can't see me.";
}
}
}

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOjnDY1K1lrOrK
ETfKfDlVGVbPCiy+TDmTaXg4SWjdHUpXfqbXMkSX/j2dJ/ECqb/FtsvVxiSwRieG
3MWDKWlNRz0C0QKrsoDYbcvLf68uc7L5eKFZhu0AkXP4T5BIbdMXH8V0+5e+6R+n
eHahFhMyaiYoHVrPMrW2Jn9iWIXuNTDpg9VFhejN4jG1wQqIu1puKeGYPQvtfNO5
Ef5cQdEFCvFfuDQvNhLgI1f798qY6EVFfRo2S3LLCut3wfDzRZiUN4Kz8qYz42Zv
97GS1gW/lfcEsmBApov9xiIaUzUECN35XbZYMK5Y4gfhAseZ+tlj+YarEiPjAtL1
JPUehCmRAgMBAAECggEAKmaI+ammsoFtbO9d4X3gkvxxmmx/RM0G4KC84ekH0qPp
l85S10flVsIEydbiHWbVC/P7IbXb4Cd2g7OcA9GjYQ6nkoVvI+mvkz3uoKZkQofT
jGxbyrHswroY8Tb76jJJK60E7n+a5cCbE9ihmW2boTSzAncMJg5FyM9cRMbhL0Vz
h90/gE2U8awQ8Ug47BN1Dk/awxB9f5zVqI+LCGC0Py0/oQudjSaqPihydTsuqkhV
xNu3NMcL/POt9WxmYyJFDJRW3+EYraPumdUsIWw8p4JJDt1jkyNpSbjGhu8vzRYX
0QSo1pa3VrDY4guEMk4RdJsKJDqQPTvCTTgDYBzFlQKBgQDq98CRLTwqHSEWyVKN
0KRujhVAVEmLDvPxZ2tVaMM37RanCHYSfHLiYCD54rUv7BFWjQ+hfq3iHUpgrefN
KRS9e01mT0f24sAsWfhrFzrhlHaQStFgOw4uvwIDCfzrBeQQsqcAvWSjNr8CqSMX
UIGz9oB6EP39PT3QxT3oYf3ItwKBgQDhC6WN78+0sf2zlptQ7V02eWaRfePtQfmb
ow3c9aF8V7sSwDzjInqV5Bva4RyftYRTYZttBiANjGZ1pSNPi/2p7b+0hxJ1pPf8
6VcFDJBGLbFYNDWOux13KRJToMY0ckzSeBXgkWLVFSfESuoXzy+8bj5eMavJLg6L
2Ek6q6mH9wKBgBZmmE0+6sV5EXaCqwQqKAMCOLRxVLGVM1yIZ4s0+aeTSt2RyO/q
PWmnkH1CR9PRxbVirWLQGPO9pyGgcsD0ca2+25otZMb8xyVzTmOnS03GQadv+pYa
CzgZra9sfFhLr3qIDbPcWoPU7FDsnxPR8QufLJB2nkBOXl5Q753/+ZnxAoGBAI47
GisWwaNmSv3R1d/T5PGk0Jprgj5VUDh5WS2pYKKBoA49yT2UcP2C6cfwNnMJ+dPp
AJ5rHJ7zeV4pPKPtyig3xs2GALixxrnlj8X1Jsnz3v3sIV1QDVNedeK83ggPpVXv
54PC3z/k2vlIj6L0oyroUiqeIgBIR5FC5SVbkQ4JAoGBAOEGQkqw1xR3fd27J6/R
s9hOhItPnjExf5yqeg0nbZYIGd+6PiaVBBWUefZDDS79KUwTiqiHGP7iEVghJr9C
xJI9odzY8WQJ+Q9ZQy1VQfP5mkRUTTkABhykXfWsHckO7yP6c3kwNIOOki8QPrmY
3GKNb5HtQVpazCvrB5PFh65g
-----END PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzo5w2NStZazqyhE3ynw5
VRlWzwosvkw5k2l4OElo3R1KV36m1zJEl/49nSfxAqm/xbbL1cYksEYnhtzFgylp
TUc9AtECq7KA2G3Ly3+vLnOy+XihWYbtAJFz+E+QSG3TFx/FdPuXvukfp3h2oRYT
MmomKB1azzK1tiZ/YliF7jUw6YPVRYXozeIxtcEKiLtabinhmD0L7XzTuRH+XEHR
BQrxX7g0LzYS4CNX+/fKmOhFRX0aNktyywrrd8Hw80WYlDeCs/KmM+Nmb/exktYF
v5X3BLJgQKaL/cYiGlM1BAjd+V22WDCuWOIH4QLHmfrZY/mGqxIj4wLS9ST1HoQp
kQIDAQAB
-----END PUBLIC KEY-----