Good day.
I'm trying to implement RSA encryption between server and Android app. My goal is:
- Share the public key with the app.
- The application performs encryption and sends the encrypted text to the server.
- The server decrypts the text with the private key.
Before the Android app, I did the same steps but with a Java Application, and it worked like a charm. But the thing is how the client was made in that application... On the server side, I did a Soap web service that returns the array of bytes of the public key:
@WebMethod(operationName = "getKey")
public byte[] getPublicKey() {
try {
// Message Context
MessageContext mctx = wsctx.getMessageContext();
// Getting parameter services
ParameterServices params = ParameterServices.NewParameterServices(getHttpServletRequestFromMsgContext(mctx), "Dumb Payload");
// Initializing KEYGEN Object
initKeyGen(params);
// Checking if there are no keys created
KEYGEN.updateKeys();
// Creating and sending public key
return KEYGEN.generatePublicKey();
} catch (Throwable e) {
LOGGER.error("Exception in KeyService", e);
return new byte[0];
}
}
In the Java Application, I created the client Interface with javax.jws like this:
@WebService(targetNamespace = "http://my.targetname.value/")
@SOAPBinding(style = SOAPBinding.Style.RPC)
public interface KeyService {
@WebMethod
byte[] getPublicKey();
}
And this is the part where I'm pretty sure that is the problem. How I retrieve the array of bytes to make the public key it´s something like this:
try {
URL url = new URL(WSDLURL);
QName qname =
new QName(TARGETNAMESPACE, SERVICENAME);
Service service = Service.create(url, qname);
KeyService keyserv = service.<KeyService>getPort(KeyService.class);
byte[] key = keyserv.getPublicKey();
X509EncodedKeySpec spec = new X509EncodedKeySpec(key);
KeyFactory kf = KeyFactory.getInstance("RSA");
publicKey = kf.generatePublic(spec);
return true;
} catch (MalformedURLException|java.security.NoSuchAlgorithmException|java.security.spec.InvalidKeySpecException e) {
e.printStackTrace();
return false;
}
This line:
byte[] key = keyserv.getPublicKey();
I'm not doing anything with the bytes, I'm just fetching it and done, whatever is throwing the method java.security.Key.getEncoded() from the server.
ANDROID KOTLIN VERSION
First of all, I tried to import JAXB to android, and I died during the attempt. Later I found the android library ksoap2 from this question. The implementation's almost the same, also the dependencies (ksoap2-android-2.5.2, kxml2-2.2.1...)
class KeyServiceKSoap {
val NAMESPACE = "http://my.targetname.value/"
val URL = "http://192.168.0.11:8080/RSATest/keyService?wsdl"
val METHOD_NAME = "obtainKey"
val SOAP_ACTION = "http://my.targetname.value/RSATest/obtainKeyRequest"
private var thread: Thread? = null
fun fetchByteArray(getKey : MutableLiveData<String>) {
thread = object : Thread() {
override fun run() {
try {
Log.d("KeyService", "Starting...")
val request = SoapObject(NAMESPACE, METHOD_NAME)
val envelope = SoapSerializationEnvelope(
SoapEnvelope.VER12
)
envelope.env = "http://schemas.xmlsoap.org/soap/envelope/"
envelope.setOutputSoapObject(request)
envelope.headerOut = Array(1) { buildAuthHeaders() }
val androidHttpTransport = HttpTransportSE(
URL,
30000
)
androidHttpTransport.call(SOAP_ACTION, envelope)
val objectResult: SoapObject = envelope.bodyIn as SoapObject
getKey.postValue(objectResult.getProperty("return").toString())
} catch (sp: SoapFault) {
sp.printStackTrace()
getKey.postValue("FAILURE")
} catch (e: Exception) {
e.printStackTrace()
getKey.postValue("FAILURE")
}
}
}
thread!!.start()
}
private fun buildAuthHeaders() : Element {
val authorization = Element().createElement(NAMESPACE, "Authorization")
authorization.addChild(Node.TEXT, "BEARER")
return authorization
}
}
THE PROBLEM
My problem here is that the response I'm getting it as a String, as you can see in this line:
objectResult.getProperty("return").toString()
If you check the Java Application client, I'm getting the array value directly, and not as a String. The issue here is that I don't know what kind of encoding is doing the method java.security.Key.getEncoded(). I mean, If I know for example that this method is returning a Base64 encoded byte array, in the android application I just need to decode like this and done:
private fun makeKey(encodedString : String) : Boolean {
return try {
val byteArr = Base64.decode(encodedString, Base64.DEFAULT);
val specifications = X509EncodedKeySpec(byteArr)
val factory: KeyFactory = KeyFactory.getInstance("RSA")
publicKey = factory.generatePublic(specifications)
true
} catch (e : Exception) {
e.printStackTrace()
false
}
}
BUT OBVIOUSLY IS NOT LIKE THIS, if I encrypt and send the encoded string to the server, and the server tries to decrypt this string, I will get javax.crypto.BadPaddingException: Decryption error, since the array of bytes was not encoded with Base64...
Is there a way to receive the byte array in ksoap2 Android like the interface created in the Java Application?
If there is no choice and I have to decode the String (to make the RSA public key in the android app)... what would be the right way to do it?
CodePudding user response:
I write some code for you, and you can see the complete code at https://github.com/XSComSoft/stack-overflow
In the main and two copies of the test code package org.exmaple.Stack74979278
respectively containing the client code and the server code
You can simple run test at class org.example.stack74979278.Main
if you open this with idea
as maven project
Communication only needs to rely on java jws
Here are some code snippets
KeyService.java
@WebService
@SOAPBinding(style = SOAPBinding.Style.RPC)
public interface KeyService {
@WebMethod
@WebResult(name = "get")
byte[] getPublicKey();
@WebMethod
@WebResult(name = "get")
String getDecodeByPrivateKey(String base64) throws Exception;
}
KeyServiceImpl.java
@WebService(endpointInterface = "org.example.stack74979278.KeyService")
public class KeyServiceImpl implements KeyService{
private RSAPublicKey puk;
private RSAPrivateKey prk;
public byte[] getPublicKey() {
return puk.getEncoded();
}
@Override
public String getDecodeByPrivateKey(String base64) throws Exception {
return RSAUtils.decryptByPrivateKey(base64, prk);
}
public KeyServiceImpl() throws Exception {
// List<Key> keyList = RSAUtils.getRSAKeyObject(1024);
// puk = (RSAPublicKey) keyList.get(0);
// prk = (RSAPrivateKey) keyList.get(1);
Properties properties = new Properties();
properties.load(KeyServiceImpl.class.getResourceAsStream("/stack74979278/keys.properties"));
puk = RSAUtils.getPublicKey(properties.getProperty("public"));
prk = RSAUtils.getPrivateKey(properties.getProperty("private"));
String publicKey = Base64.getEncoder().encodeToString(puk.getEncoded());
String privateKey = Base64.getEncoder().encodeToString(prk.getEncoded());
System.out.println("publicKey");
System.out.println(publicKey);
System.out.println("privateKey");
System.out.println(privateKey);
}
public static void main(String[] args) throws Exception {
Endpoint.publish("http://localhost/test", new KeyServiceImpl());
}
}
RSAUtil.java
public class RSAUtils {
// 加密算法
private final static String ALGORITHM_RSA = "RSA";
/**
* 直接生成公钥、私钥对象
*
* @param modulus
*
* @throws NoSuchAlgorithmException
*
*/
public static List<Key> getRSAKeyObject(int modulus) throws NoSuchAlgorithmException{
List<Key> keyList = new ArrayList<>(2);
// 创建RSA密钥生成器
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM_RSA);
// 设置密钥的大小,此处是RSA算法的模长 = 最大加密数据的大小
keyPairGen.initialize(modulus);
KeyPair keyPair = keyPairGen.generateKeyPair();
// keyPair.getPublic() 生成的是RSAPublic的是咧
keyList.add(keyPair.getPublic());
// keyPair.getPrivate() 生成的是RSAPrivateKey的实例
keyList.add(keyPair.getPrivate());
return keyList;
}
/**
* 生成公钥、私钥的字符串
* 方便传输
*
* @param modulus 模长
* @return
* @throws NoSuchAlgorithmException
*/
public static List<String> getRSAKeyString(int modulus) throws NoSuchAlgorithmException{
List<String> keyList = new ArrayList<>(2);
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM_RSA);
keyPairGen.initialize(modulus);
KeyPair keyPair = keyPairGen.generateKeyPair();
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
keyList.add(publicKey);
keyList.add(privateKey);
return keyList;
}
public static RSAPublicKey getPublicKey(String publicKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
return (RSAPublicKey) keyFactory.generatePublic(spec);
}
public static RSAPublicKey getPublicKey(byte[] keyBytes) throws Exception {
RSAPublicKey publicKey =
(RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(keyBytes));
return publicKey;
}
public static RSAPrivateKey getPrivateKey(String privateKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
return getPrivateKey(keyBytes);
}
public static RSAPrivateKey getPrivateKey(byte[] keyBytes) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
return (RSAPrivateKey) keyFactory.generatePrivate(spec);
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
* @throws Exception
*/
public static String encryptByPublicKey(String data, RSAPublicKey publicKey)
throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM_RSA);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 模长n转换成字节数
int modulusSize = publicKey.getModulus().bitLength() / 8;
// PKCS Padding长度为11字节,所以实际要加密的数据不能要 - 11byte
int maxSingleSize = modulusSize - 11;
// 切分字节数组,每段不大于maxSingleSize
byte[][] dataArray = splitArray(data.getBytes(), maxSingleSize);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 分组加密,并将加密后的内容写入输出字节流
for (byte[] s : dataArray) {
out.write(cipher.doFinal(s));
}
// 使用Base64将字节数组转换String类型
return Base64.getEncoder().encodeToString(out.toByteArray());
}
/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
* @throws Exception
*/
public static String decryptByPrivateKey(String data, RSAPrivateKey privateKey)
throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM_RSA);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// RSA加密算法的模长 n
int modulusSize = privateKey.getModulus().bitLength() / 8;
byte[] dataBytes = data.getBytes();
// 之前加密的时候做了转码,此处需要使用Base64进行解码
byte[] decodeData = Base64.getDecoder().decode(dataBytes);
// 切分字节数组,每段不大于modulusSize
byte[][] splitArrays = splitArray(decodeData, modulusSize);
ByteArrayOutputStream out = new ByteArrayOutputStream();
for(byte[] arr : splitArrays){
out.write(cipher.doFinal(arr));
}
return new String(out.toByteArray());
}
/**
* 按指定长度切分数组
*
* @param data
* @param len 单个字节数组长度
* @return
*/
private static byte[][] splitArray(byte[] data,int len){
int dataLen = data.length;
if (dataLen <= len) {
return new byte[][]{data};
}
byte[][] result = new byte[(dataLen-1)/len 1][];
int resultLen = result.length;
for (int i = 0; i < resultLen; i ) {
if (i == resultLen - 1) {
int slen = dataLen - len * i;
byte[] single = new byte[slen];
System.arraycopy(data, len * i, single, 0, slen);
result[i] = single;
break;
}
byte[] single = new byte[len];
System.arraycopy(data, len * i, single, 0, len);
result[i] = single;
}
return result;
}
}
and the client generator by cmdwsimport -keep -p org.example.stack74979278 http://localhost/test?wsdl
, see detail a
the client test class
@Log
public class Main {
@Test
public void getPublicKey(){
byte[] bytes = new KeyServiceImplService().getKeyServiceImplPort().getPublicKey();
System.out.println(bytes);
}
@Test
public void getDecode() throws Exception {
KeyService keyService = new KeyServiceImplService().getKeyServiceImplPort();
byte[] bytes = keyService.getPublicKey();
RSAPublicKey publicKey = RSAUtils.getPublicKey(bytes);
String encrypt = "The message encrypt";
String str = keyService.getDecodeByPrivateKey(RSAUtils.encryptByPublicKey(encrypt, publicKey));
log.info(str);
}
}
Now you can get the public key byte
for android client and send server the encrypted base64 string
CodePudding user response:
Well I don't know how to start this. Apparently, the way to receive an array of bytes as I was doing right now, is the right one.
With ksoap2 you get the response string, and then decode the String to get your array of bytes.
private fun makeKey(encodedString : String) : Boolean {
return try {
val byteArr = Base64.decode(encodedString, Base64.DEFAULT) <--- OK
val specifications = X509EncodedKeySpec(byteArr)
val factory: KeyFactory = KeyFactory.getInstance("RSA")
publicKey = factory.generatePublic(specifications)
true
} catch (e : Exception) {
e.printStackTrace()
false
}
}
How come I´m so sure of this? Well, is because what was killing me had nothing to do with encoding. It was how I was initializing the Cipher. As I said before, with Java applications, everything was working correctly, but when using the same implementation in android applications (Java Application <=> Android Application), the decryption failed.
How I was initializing the Cipher was like this:
Cipher.getInstance("RSA")
According with a comment of this question If you initialize it this way, what will happen is that both, the Android App and the Server will use their independent implementations. This is why it was always failing at the moment of decryption.
To make all things clear:
If you are trying to do the same as me, try to initialize all Cipher with RSA/ECB/PKCS1Padding in both sides (Server and App)
Cipher.getInstance("RSA/ECB/PKCS1Padding")
Other things like KeyFactory and KeyPairGenerator has to be initialized only with "RSA".
KeyPairGenerator.getInstance("RSA")
KeyFactory.getInstance("RSA")
P.D: I NEVER encoded the array of bytes of the key to Base64 in my SOAP Service... Which makes me think that base64 encoding is done by default at some point