diff --git a/key-pair-loader/README.md b/key-pair-loader/README.md index 2a8186d..3d32826 100644 --- a/key-pair-loader/README.md +++ b/key-pair-loader/README.md @@ -43,22 +43,71 @@ ZyYNcH60ONRWjMqlQXozWMb2i7WphKxf8kopp42nzCflWQod+JQY+hM/EQ== -----END PUBLIC KEY----- ``` -#### Convert private key to EC formats which could be acceptable by Java +## RSA-based algorithm -Java's `PKCS8EncodedKeySpec` requires the private key to be in PKCS#8 format, while OpenSSL by -default generates private keys in traditional PEM format. To convert the private key, run the -following command: +### Generate key pair + +#### Generate private key + +Generate a private key by `genpkey` command provided by OpenSSL: ```shell -openssl pkcs8 -topk8 -inform PEM -outform PEM -in ec_private_key.pem -out ec_private_key_pkcs8.pem -nocrypt +openssl genpkey -algorithm RSA -out rsa_private_key.pem -pkeyopt rsa_keygen_bits:2048 ``` -The converted private key will look like this: +The output of this command is a file called `rsa_private_key.pem` and its content looks like the +following: ```text -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7 -+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn -Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4VIFYJFMAs15j +J3V3IicHd7sI2TIFqTZME40zlOlVAlPKLZmTQvZFLNgaUAAsvPi5i1DR2ywwK6Al +BfnwVnzvmDXC5mKHOz4oxOQVA6Nlp2yVaQMzidmfYNSkMtcv/4HRPsatc7K/M5l6 +pCP20DVRjkikBdIy8e9w+x6BrIFp5Q8PZc/X2BGNAUMMYACdeYH5R/A0CxqkND13 +esc4gkynMOrvZrZGHCz51usfSCqyDWWwsN+GG6LYWia4GkNlS0erQnP8gS93dfjl +e96BIfy3z7Iv+kUrf5ikNW2P8jMxLAv6LO+dcUAu9k477wIAF7Iq5KMuH/otsDOu ++h+2qXmBAgMBAAECggEAdRqcmC0g+y6arxV3fkObthjPGYAa57KBCWUa7B0n30+m +pavVRS2Jpttb2SSqwG4ouI6rARti/iBEd9EWqTCP4AieKZetFOpqCJ24lPRPRGus +d9S6jr5N4qut+vSCp37NABijZj4uJ540nTH0R7qtuhTnynl4Q0/1wwiYvTvVF1Lg +dn+I/8aRbshwDhdAOWOUe6GL7/eaCYgN8/UmlKIpp8tg0w2iWxbaFiR7gZiM41LA +M6SXXfcCas+ZVXsGbzQ3SNiVurCGuuRNcCScXS3/WoEDIb3cNtp49iOmQS+nmEoo +wh4uiEd+0+BrzxngS4o5+mKnHJnwgY0+veGVYLMR5QKBgQD9WKQmevMDU5c+NPq9 +8jaR457Fuxq1gwzeFNJdWfOc/K2LEWh+nFNFCb++EboEj6FdxWaWNMxbrmJps5gs +EoBUYy/Tl7UycDqDfiYLmDdTsf2pVjjh9jaIADiLcJ8S6wwJMZKub7Tp8UVkenAl +535MqShLUC11Y7VxLb3Tsll4XwKBgQD67mm6iCmshr/eszPfNE3ylZ+PiNa7nat7 +N7lQzBIiRJflT1kmVidC5gE+jASqH728ChkZZKxbHsjxpmWdAhLOITdXoTB4sDsd +wtV1lxkXxK9FnrpFvO3y1wZ/QsD3Z2KXxHYZqawkUETO9F3nqAXW0b2GDar5Qiyo +J3Tx/43aHwKBgDC0NMJtCoDONhowZy/S+6iqQKC0qprQec3L5PErVMkOTnKYwyTr ++pogGKt6ju9HiXcUdvdTaSIK8UJu00dNuzv94XjlBmGO78DNpJTAC4rcge5m9AKE +qdEVcclkukARzbuKuy8rrHT4/CUn4J141m/4aRWpcUPLCluato6XD9ozAoGBANvf +JhOFFgcPd3YazfvpZ9eE1XA+tfFlYYmxNRcgCU+vjO0oDvSxjutmgHae18N91pG6 +w21lskSRf/+GDwl5dKLbphOJsOA/gz07qDDGOf2CoRW+1Hcg6drcINxH0K+4DkLv +qZApBSY4k2JH6zR+HMeztn6M4WBRZLHfCPC3PUN/AoGAA3AoHbLTZvqMIKSDkP4Y +U/tTsSFDY4aYo7LG/jk8af3oPU3KyGh4ZFBd6aMmXbS8f8FjvmrM+/e+y9OOGAlq +iOl0hYrs5cJSMLW6i4KnJYuYbMkgmk3bN2t9apu64xKR94gbPrI6AGnPZp+iIzp0 +hXKe4HcuhQ3G0a2hjayiQ84= -----END PRIVATE KEY----- +``` + +#### Generate public key by private key + +Export public key from private key by OpenSSL: + +```shell +openssl pkey -in rsa_private_key.pem -pubout -out rsa_public_key.pem +``` + +The output of this command is a file called `rsa_public_key.pem` and its content looks like the +following: + +```text +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+FSBWCRTALNeYyd1dyIn +B3e7CNkyBak2TBONM5TpVQJTyi2Zk0L2RSzYGlAALLz4uYtQ0dssMCugJQX58FZ8 +75g1wuZihzs+KMTkFQOjZadslWkDM4nZn2DUpDLXL/+B0T7GrXOyvzOZeqQj9tA1 +UY5IpAXSMvHvcPsegayBaeUPD2XP19gRjQFDDGAAnXmB+UfwNAsapDQ9d3rHOIJM +pzDq72a2Rhws+dbrH0gqsg1lsLDfhhui2FomuBpDZUtHq0Jz/IEvd3X45XvegSH8 +t8+yL/pFK3+YpDVtj/IzMSwL+izvnXFALvZOO+8CABeyKuSjLh/6LbAzrvoftql5 +gQIDAQAB +-----END PUBLIC KEY----- ``` \ No newline at end of file diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java index 0fa6a63..097ae8e 100644 --- a/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java +++ b/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java @@ -49,4 +49,29 @@ public interface KeyLoader { */ PublicKey loadPublicKey(String pemKeyText); + /** + * Retrieves the raw content of a PEM formatted key by removing unnecessary headers, footers, + * and new line characters. + * + *
+ * This method processes the provided PEM key text to return a cleaned string that contains + * only the key content. The method strips away the + * {@code "-----BEGIN (EC )?(PRIVATE|PUBLIC) KEY-----"} and + * {@code "-----END (EC )?(PRIVATE|PUBLIC) KEY-----"} lines, as well as any new line characters, + * resulting in a continuous string representation of the key, which can be used for further + * cryptographic operations. + * + * @param pemKeyText the PEM formatted key as a string, which may include headers, footers and + * line breaks + * @return a string containing the raw key content devoid of any unnecessary formatting + * or whitespace + */ + default String getRawContent(String pemKeyText) { + // remove all unnecessary parts of the pem key text + return pemKeyText + .replaceAll("-----BEGIN (EC )?(PRIVATE|PUBLIC) KEY-----", "") + .replaceAll("-----END (EC )?(PRIVATE|PUBLIC) KEY-----", "") + .replaceAll("\n", ""); + } + } diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java index b94b1f7..96daa58 100644 --- a/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java +++ b/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java @@ -82,11 +82,7 @@ public class EcKeyLoader implements KeyLoader { @Override public ECPrivateKey loadPrivateKey(String pemKeyText) { try { - // remove all unnecessary parts of the pem key text - pemKeyText = pemKeyText - .replaceAll("-----BEGIN (EC )?PRIVATE KEY-----", "") - .replaceAll("-----END (EC )?PRIVATE KEY-----", "") - .replaceAll("\n", ""); + pemKeyText = getRawContent(pemKeyText); var decodedKeyString = decoder.decode(pemKeyText); var keySpec = new PKCS8EncodedKeySpec(decodedKeyString); @@ -112,11 +108,7 @@ public class EcKeyLoader implements KeyLoader { @Override public ECPublicKey loadPublicKey(String pemKeyText) { try { - // remove all unnecessary parts of the pem key text - pemKeyText = pemKeyText - .replaceAll("-----BEGIN (EC )?PUBLIC KEY-----", "") - .replaceAll("-----END (EC )?PUBLIC KEY-----", "") - .replaceAll("\n", ""); + pemKeyText = getRawContent(pemKeyText); var keyBytes = decoder.decode(pemKeyText); var spec = new X509EncodedKeySpec(keyBytes); var key = keyFactory.generatePublic(spec); diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java new file mode 100644 index 0000000..7f1b6e4 --- /dev/null +++ b/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.security.impl; + +import com.onixbyte.security.KeyLoader; +import com.onixbyte.security.exception.KeyLoadingException; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * A class responsible for loading RSA keys from PEM formatted text. + *
+ * This class implements the {@link KeyLoader} interface and provides methods to load both private + * and public RSA keys. The keys are expected to be in the standard PEM format, which includes + * Base64-encoded key content surrounded by header and footer lines. The class handles the decoding + * of Base64 content and the generation of keys using the RSA key factory. + *
+ * Any exceptions encountered during the loading process are encapsulated in a + * {@link KeyLoadingException}, allowing for flexible error handling. + * + * @author siujamo + * @see KeyLoader + * @see KeyLoadingException + */ +public class RsaKeyLoader implements KeyLoader { + + private final Base64.Decoder decoder; + private final KeyFactory keyFactory; + + /** + * Constructs an instance of {@code RsaKeyLoader}. + *
+ * This constructor initialises the Base64 decoder and the RSA {@link KeyFactory}. It may throw + * a {@link KeyLoadingException} if the RSA algorithm is not available. + */ + public RsaKeyLoader() { + try { + this.decoder = Base64.getDecoder(); + this.keyFactory = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new KeyLoadingException(e); + } + } + + /** + * Loads an RSA private key from a given PEM formatted key text. + *
+ * This method extracts the raw key content from the provided PEM text, decodes the + * Base64-encoded content, and generates an instance of {@link RSAPrivateKey}. If the key cannot + * be loaded due to invalid specifications or types, a {@link KeyLoadingException} is thrown. + * + * @param pemKeyText the PEM formatted private key text + * @return an instance of {@link RSAPrivateKey} + * @throws KeyLoadingException if the key loading process encounters an error + */ + @Override + public RSAPrivateKey loadPrivateKey(String pemKeyText) { + // Extract the raw key content + var rawKeyContent = getRawContent(pemKeyText); + + // Decode the Base64-encoded content + var keyBytes = decoder.decode(rawKeyContent); + + // Create a PKCS8EncodedKeySpec from the decoded bytes + var keySpec = new PKCS8EncodedKeySpec(keyBytes); + + try { + // Get an RSA KeyFactory and generate the private key + var _key = keyFactory.generatePrivate(keySpec); + if (_key instanceof RSAPrivateKey key) { + return key; + } else { + throw new KeyLoadingException("Unable to load private key from pem-formatted key text."); + } + } catch (InvalidKeySpecException e) { + throw new KeyLoadingException("Key spec is invalid.", e); + } + } + + /** + * Loads an RSA public key from a given PEM formatted key text. + *
+ * This method extracts the raw key content from the provided PEM text, decodes the + * Base64-encoded content, and generates an instance of {@link RSAPublicKey}. If the key cannot + * be loaded due to invalid specifications or types, a {@link KeyLoadingException} is thrown. + * + * @param pemKeyText the PEM formatted public key text + * @return an instance of {@link RSAPublicKey} + * @throws KeyLoadingException if the key loading process encounters an error + */ + @Override + public RSAPublicKey loadPublicKey(String pemKeyText) { + // Extract the raw key content + var rawKeyContent = getRawContent(pemKeyText); + + // Decode the Base64-encoded content + var keyBytes = decoder.decode(rawKeyContent); + + // Create an X509EncodedKeySpec from the decoded bytes + var keySpec = new X509EncodedKeySpec(keyBytes); + + // Get an RSA KeyFactory and generate the public key + try { + var _key = keyFactory.generatePublic(keySpec); + if (_key instanceof RSAPublicKey key) { + return key; + } else { + throw new KeyLoadingException("Unable to load public key from pem-formatted key text."); + } + } catch (InvalidKeySpecException e) { + throw new KeyLoadingException("Key spec is invalid.", e); + } + } +} diff --git a/key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java b/key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java deleted file mode 100644 index ca57f74..0000000 --- a/key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024-2024 OnixByte. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.onixbyte.security; - -import com.onixbyte.security.impl.EcKeyLoader; -import org.junit.jupiter.api.Test; - -public class KeyPairLoaderTest { - - @Test - public void test() { - var keyLoader = new EcKeyLoader(); - // The following key pair is only used for test only, and is already exposed to public. - // DO NOT USE THEM FOR PRODUCTION! - var privateKey = keyLoader.loadPrivateKey(""" - -----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7 - +PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn - Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R - -----END PRIVATE KEY----- - """); - var publicKey = keyLoader.loadPublicKey(""" - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZAOxkWNouIIPKsdF1YgF1D/XD8Pa - ZyYNcH60ONRWjMqlQXozWMb2i7WphKxf8kopp42nzCflWQod+JQY+hM/EQ== - -----END PUBLIC KEY----- - """); - } - -} diff --git a/key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem b/key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem deleted file mode 100644 index 02dfcc8..0000000 --- a/key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7 -+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn -Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R ------END PRIVATE KEY----- diff --git a/key-pair-loader/src/test/resources/rsa_private_key.pem b/key-pair-loader/src/test/resources/rsa_private_key.pem new file mode 100644 index 0000000..f8b0fc4 --- /dev/null +++ b/key-pair-loader/src/test/resources/rsa_private_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4VIFYJFMAs15j +J3V3IicHd7sI2TIFqTZME40zlOlVAlPKLZmTQvZFLNgaUAAsvPi5i1DR2ywwK6Al +BfnwVnzvmDXC5mKHOz4oxOQVA6Nlp2yVaQMzidmfYNSkMtcv/4HRPsatc7K/M5l6 +pCP20DVRjkikBdIy8e9w+x6BrIFp5Q8PZc/X2BGNAUMMYACdeYH5R/A0CxqkND13 +esc4gkynMOrvZrZGHCz51usfSCqyDWWwsN+GG6LYWia4GkNlS0erQnP8gS93dfjl +e96BIfy3z7Iv+kUrf5ikNW2P8jMxLAv6LO+dcUAu9k477wIAF7Iq5KMuH/otsDOu ++h+2qXmBAgMBAAECggEAdRqcmC0g+y6arxV3fkObthjPGYAa57KBCWUa7B0n30+m +pavVRS2Jpttb2SSqwG4ouI6rARti/iBEd9EWqTCP4AieKZetFOpqCJ24lPRPRGus +d9S6jr5N4qut+vSCp37NABijZj4uJ540nTH0R7qtuhTnynl4Q0/1wwiYvTvVF1Lg +dn+I/8aRbshwDhdAOWOUe6GL7/eaCYgN8/UmlKIpp8tg0w2iWxbaFiR7gZiM41LA +M6SXXfcCas+ZVXsGbzQ3SNiVurCGuuRNcCScXS3/WoEDIb3cNtp49iOmQS+nmEoo +wh4uiEd+0+BrzxngS4o5+mKnHJnwgY0+veGVYLMR5QKBgQD9WKQmevMDU5c+NPq9 +8jaR457Fuxq1gwzeFNJdWfOc/K2LEWh+nFNFCb++EboEj6FdxWaWNMxbrmJps5gs +EoBUYy/Tl7UycDqDfiYLmDdTsf2pVjjh9jaIADiLcJ8S6wwJMZKub7Tp8UVkenAl +535MqShLUC11Y7VxLb3Tsll4XwKBgQD67mm6iCmshr/eszPfNE3ylZ+PiNa7nat7 +N7lQzBIiRJflT1kmVidC5gE+jASqH728ChkZZKxbHsjxpmWdAhLOITdXoTB4sDsd +wtV1lxkXxK9FnrpFvO3y1wZ/QsD3Z2KXxHYZqawkUETO9F3nqAXW0b2GDar5Qiyo +J3Tx/43aHwKBgDC0NMJtCoDONhowZy/S+6iqQKC0qprQec3L5PErVMkOTnKYwyTr ++pogGKt6ju9HiXcUdvdTaSIK8UJu00dNuzv94XjlBmGO78DNpJTAC4rcge5m9AKE +qdEVcclkukARzbuKuy8rrHT4/CUn4J141m/4aRWpcUPLCluato6XD9ozAoGBANvf +JhOFFgcPd3YazfvpZ9eE1XA+tfFlYYmxNRcgCU+vjO0oDvSxjutmgHae18N91pG6 +w21lskSRf/+GDwl5dKLbphOJsOA/gz07qDDGOf2CoRW+1Hcg6drcINxH0K+4DkLv +qZApBSY4k2JH6zR+HMeztn6M4WBRZLHfCPC3PUN/AoGAA3AoHbLTZvqMIKSDkP4Y +U/tTsSFDY4aYo7LG/jk8af3oPU3KyGh4ZFBd6aMmXbS8f8FjvmrM+/e+y9OOGAlq +iOl0hYrs5cJSMLW6i4KnJYuYbMkgmk3bN2t9apu64xKR94gbPrI6AGnPZp+iIzp0 +hXKe4HcuhQ3G0a2hjayiQ84= +-----END PRIVATE KEY----- diff --git a/key-pair-loader/src/test/resources/rsa_public_key.pem b/key-pair-loader/src/test/resources/rsa_public_key.pem new file mode 100644 index 0000000..edde855 --- /dev/null +++ b/key-pair-loader/src/test/resources/rsa_public_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+FSBWCRTALNeYyd1dyIn +B3e7CNkyBak2TBONM5TpVQJTyi2Zk0L2RSzYGlAALLz4uYtQ0dssMCugJQX58FZ8 +75g1wuZihzs+KMTkFQOjZadslWkDM4nZn2DUpDLXL/+B0T7GrXOyvzOZeqQj9tA1 +UY5IpAXSMvHvcPsegayBaeUPD2XP19gRjQFDDGAAnXmB+UfwNAsapDQ9d3rHOIJM +pzDq72a2Rhws+dbrH0gqsg1lsLDfhhui2FomuBpDZUtHq0Jz/IEvd3X45XvegSH8 +t8+yL/pFK3+YpDVtj/IzMSwL+izvnXFALvZOO+8CABeyKuSjLh/6LbAzrvoftql5 +gQIDAQAB +-----END PUBLIC KEY-----