First draft of an external TLS provider app.
@ -12,7 +12,11 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
classpath 'com.android.tools.build:gradle:3.1.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
// ExternalCertificateProvider.aidl
|
||||||
|
package de.blinkt.openvpn.api;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is very simple interface that is specialised to have only the minimal set of crypto
|
||||||
|
* operation that are needed for OpenVPN to authenticate with an external certificate
|
||||||
|
*/
|
||||||
|
interface ExternalCertificateProvider {
|
||||||
|
/**
|
||||||
|
* Requests signing the data with RSA/ECB/PKCS1PADDING
|
||||||
|
* for RSA certficate and with NONEwithECDSA for EC certificates
|
||||||
|
* @parm alias the parameter that
|
||||||
|
*/
|
||||||
|
byte[] getSignedData(String alias, in byte[] data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a
|
||||||
|
*/
|
||||||
|
String[] getCertificateChain(in String alias);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* request an Intent that should be started when user uses the select certificate box
|
||||||
|
* the already selected alias will be provided in the extra android.security.extra.KEY_ALIAS
|
||||||
|
* if applicable
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
@ -18,7 +18,7 @@ interface IOpenVPNAPIService {
|
|||||||
boolean addVPNProfile (String name, String config);
|
boolean addVPNProfile (String name, String config);
|
||||||
|
|
||||||
/** start a profile using a config as inline string. Make sure that all needed data is inlined,
|
/** start a profile using a config as inline string. Make sure that all needed data is inlined,
|
||||||
* e.g., using <ca>...</ca> or <auth-user-data>...</auth-user-data>
|
* e.g., using <ca>...</ca> or <auth-user-pass>...</auth-user-pass>
|
||||||
* See the OpenVPN manual page for more on inlining files */
|
* See the OpenVPN manual page for more on inlining files */
|
||||||
void startVPN (in String inlineconfig);
|
void startVPN (in String inlineconfig);
|
||||||
|
|
||||||
|
@ -4,4 +4,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
include ':main'
|
include ':main'
|
||||||
|
include ':tlsexternalcertprovider'
|
||||||
include ':remoteExample'
|
include ':remoteExample'
|
||||||
|
1
tlsexternalcertprovider/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
54
tlsexternalcertprovider/build.gradle
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
* Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
bouncycastleVersion = '1.49'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 27
|
||||||
|
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "de.blinkt.externalcertprovider"
|
||||||
|
minSdkVersion 16
|
||||||
|
targetSdkVersion 27
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility 1.8
|
||||||
|
sourceCompatibility 1.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation (
|
||||||
|
'org.bouncycastle:bcprov-jdk15on:' + bouncycastleVersion,
|
||||||
|
'org.bouncycastle:bcprov-ext-jdk15on:' + bouncycastleVersion,
|
||||||
|
'org.bouncycastle:bcpkix-jdk15on:' + bouncycastleVersion,
|
||||||
|
'org.bouncycastle:bcmail-jdk15on:' + bouncycastleVersion,
|
||||||
|
'org.bouncycastle:bcpg-jdk15on:' + bouncycastleVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
21
tlsexternalcertprovider/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
* Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.blinkt.externalcertprovider;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
public void useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("de.blinkt.externalcertprovider", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
43
tlsexternalcertprovider/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="de.blinkt.externalcertprovider">
|
||||||
|
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
OpenVPN for android will search for all application that have a activity that fulfils this
|
||||||
|
intent. It will be called when the user presses the select button and should return the selected
|
||||||
|
alias in the android.security.extra.KEY_ALIAS extra on setResult.
|
||||||
|
|
||||||
|
the already selected alias will be provided in the extra android.security.extra.KEY_ALIAS
|
||||||
|
if applicable
|
||||||
|
-->
|
||||||
|
<activity android:name=".SelectCertificateActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<category android:name="de.blinkt.openvpn.CertProviderSelect"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".ExternalCertService"
|
||||||
|
tools:ignore="ExportedService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="de.blinkt.openvpn.api.ExternalCertificateProvider" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,28 @@
|
|||||||
|
// ExternalCertificateProvider.aidl
|
||||||
|
package de.blinkt.openvpn.api;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is very simple interface that is specialised to have only the minimal set of crypto
|
||||||
|
* operation that are needed for OpenVPN to authenticate with an external certificate
|
||||||
|
*/
|
||||||
|
interface ExternalCertificateProvider {
|
||||||
|
/**
|
||||||
|
* Requests signing the data with RSA/ECB/PKCS1PADDING
|
||||||
|
* for RSA certficate and with NONEwithECDSA for EC certificates
|
||||||
|
* @parm alias the parameter that
|
||||||
|
*/
|
||||||
|
byte[] getSignedData(String alias, in byte[] data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a
|
||||||
|
*/
|
||||||
|
String[] getCertificateChain(in String alias);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* request an Intent that should be started when user uses the select certificate box
|
||||||
|
* the already selected alias will be provided in the extra android.security.extra.KEY_ALIAS
|
||||||
|
* if applicable
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
* Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.blinkt.externalcertprovider;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import de.blinkt.openvpn.api.ExternalCertificateProvider;
|
||||||
|
import org.bouncycastle.openssl.PEMKeyPair;
|
||||||
|
import org.bouncycastle.openssl.PEMParser;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a VERY basic implementation.
|
||||||
|
* It does not even check if the service is even allowed to use the API
|
||||||
|
* see ExternalOpenVPNService for an example of checking caller's creditionals
|
||||||
|
*/
|
||||||
|
public class ExternalCertService extends Service {
|
||||||
|
|
||||||
|
String pemkey = "-----BEGIN PRIVATE KEY-----\n" +
|
||||||
|
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDsZY/pEsIaW+ZW\n" +
|
||||||
|
"KgipgjotRHijADuwn+cnEECT7/HMPqCqBKKAGxOp5v6B1nCQqNjU3jDYNQDSvmLw\n" +
|
||||||
|
"SNr8FY3Exm0LmfErgwAK0yojC+XN+TXfQ2EVcq2VmPZzIUFeoN1HJ6DVmtRBqBwd\n" +
|
||||||
|
"VyBxF4/3KJ4+B87s1Q5CTx50R45HndIUKCcsFBD10Za1k3SE7/kE3o1Kb993q+rR\n" +
|
||||||
|
"WNNE/loEAf8Gepf3/eNXSOHw30ATn2YjWuNVVD1UOe4A+RLx0t90LrrX8I3G3RhY\n" +
|
||||||
|
"HJMiC3X6qNbgtS8tudT+uU+G4nVIFmD7P8m0MEIp+zuzK7lZgWpG80WDv/3VGv83\n" +
|
||||||
|
"DG9b/WHxAgMBAAECggEBAIOdaCpUD02trOh8LqZxowJhBOl7z7/ex0uweMPk67LT\n" +
|
||||||
|
"i5AdVHwOlzwZJ8oSIknoOBEMRBWcLQEojt1JMuL2/R95emzjIKshHHzqZKNulFvB\n" +
|
||||||
|
"TIUpdnwChTKtH0mqUkLlPU3Ienty4IpNlpmfUKimfbkWHERdBJBHbtDsTABhdo3X\n" +
|
||||||
|
"9pCF/yRKqJS2Fy/Mkl3gv1y/NB1OL4Jhl7vQbf+kmgfQN2qdOVe2BOKQ8NlPUDmE\n" +
|
||||||
|
"/1XNIDaE3s6uvUaoFfwowzsCCwN2/8QrRMMKkjvV+lEVtNmQdYxj5Xj5IwS0vkK0\n" +
|
||||||
|
"6icsngW87cpZxxc1zsRWcSTloy5ohub4FgKhlolmigECgYEA+cBlxzLvaMzMlBQY\n" +
|
||||||
|
"kCac9KQMvVL+DIFHlZA5i5L/9pRVp4JJwj3GUoehFJoFhsxnKr8HZyLwBKlCmUVm\n" +
|
||||||
|
"VxnshRWiAU18emUmeAtSGawlAS3QXhikVZDdd/L20YusLT+DXV81wlKR97/r9+17\n" +
|
||||||
|
"klQOLkSdPm9wcMDOWMNHX8bUg8kCgYEA8k+hQv6+TR/+Beao2IIctFtw/EauaJiJ\n" +
|
||||||
|
"wW5ql1cpCLPMAOQUvjs0Km3zqctfBF8mUjdkcyJ4uhL9FZtfywY22EtRIXOJ/8VR\n" +
|
||||||
|
"we65mVo6RLR8YVM54sihanuFOnlyF9LIBWB+9pUfh1/Y7DSebh7W73uxhAxQhi3Y\n" +
|
||||||
|
"QwfIQIFd8OkCgYBalH4VXhLYhpaYCiXSej6ot6rrK2N6c5Tb2MAWMA1nh+r84tMP\n" +
|
||||||
|
"gMoh+pDgYPAqMI4mQbxUmqZEeoLuBe6VHpDav7rPECRaW781AJ4ZM4cEQ3Jz/inz\n" +
|
||||||
|
"4qOAMn10CF081/Ez9ykPPlU0bsYNWHNd4eB2xWnmUBKOwk7UgJatVPaUiQKBgQCI\n" +
|
||||||
|
"f18CVGpzG9CHFnaK8FCnMNOm6VIaTcNcGY0mD81nv5Dt943P054BQMsAHTY7SjZW\n" +
|
||||||
|
"HioRyZtkhonXAB2oSqnekh7zzxgv4sG5k3ct8evdBCcE1FNJc2eqikZ0uDETRoOy\n" +
|
||||||
|
"s7cRxNNr+QxDkyikM+80HOPU1PMPgwfOSrX90GJQ8QKBgEBKohGMV/sNa4t14Iau\n" +
|
||||||
|
"qO8aagoqh/68K9GFXljsl3/iCSa964HIEREtW09Qz1w3dotEgp2w8bsDa+OwWrLy\n" +
|
||||||
|
"0SY7T5jRViM3cDWRlUBLrGGiL0FiwsfqiRiji60y19erJgrgyGVIb1kIgIBRkgFM\n" +
|
||||||
|
"2MMweASzTmZcri4PA/5C0HYb\n" +
|
||||||
|
"-----END PRIVATE KEY-----\n";
|
||||||
|
|
||||||
|
String[] certchain = new String[]{"-----BEGIN CERTIFICATE-----\n" +
|
||||||
|
"MIIFFDCCAvygAwIBAgIBAjANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJLRzEL\n" +
|
||||||
|
"MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t\n" +
|
||||||
|
"VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMB4XDTE0MTAy\n" +
|
||||||
|
"MjIxNTk1M1oXDTI0MTAxOTIxNTk1M1owajELMAkGA1UEBhMCS0cxCzAJBgNVBAgT\n" +
|
||||||
|
"Ak5BMRUwEwYDVQQKEwxPcGVuVlBOLVRFU1QxFDASBgNVBAMTC1Rlc3QtQ2xpZW50\n" +
|
||||||
|
"MSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21haW4wggEiMA0GCSqGSIb3\n" +
|
||||||
|
"DQEBAQUAA4IBDwAwggEKAoIBAQDsZY/pEsIaW+ZWKgipgjotRHijADuwn+cnEECT\n" +
|
||||||
|
"7/HMPqCqBKKAGxOp5v6B1nCQqNjU3jDYNQDSvmLwSNr8FY3Exm0LmfErgwAK0yoj\n" +
|
||||||
|
"C+XN+TXfQ2EVcq2VmPZzIUFeoN1HJ6DVmtRBqBwdVyBxF4/3KJ4+B87s1Q5CTx50\n" +
|
||||||
|
"R45HndIUKCcsFBD10Za1k3SE7/kE3o1Kb993q+rRWNNE/loEAf8Gepf3/eNXSOHw\n" +
|
||||||
|
"30ATn2YjWuNVVD1UOe4A+RLx0t90LrrX8I3G3RhYHJMiC3X6qNbgtS8tudT+uU+G\n" +
|
||||||
|
"4nVIFmD7P8m0MEIp+zuzK7lZgWpG80WDv/3VGv83DG9b/WHxAgMBAAGjgcgwgcUw\n" +
|
||||||
|
"CQYDVR0TBAIwADAdBgNVHQ4EFgQU0rQ2D7H83aXqKvfHI4n64/p6RB0wgZgGA1Ud\n" +
|
||||||
|
"IwSBkDCBjYAUK0DlyX319JY46S/jL9lAZMmOBZuhaqRoMGYxCzAJBgNVBAYTAktH\n" +
|
||||||
|
"MQswCQYDVQQIEwJOQTEQMA4GA1UEBxMHQklTSEtFSzEVMBMGA1UEChMMT3BlblZQ\n" +
|
||||||
|
"Ti1URVNUMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21haW6CCQChTt76\n" +
|
||||||
|
"kPKugTANBgkqhkiG9w0BAQsFAAOCAgEAf+D+hKfs32KlzTzB5kKxMRLwudqnnj+9\n" +
|
||||||
|
"llK2/FV0ZD7k/36q9z4GGF9zhfjI4GcbTZfKBdA3BzNkm+Z4dxSaVbsqrMN/yRUI\n" +
|
||||||
|
"g1zIwmHTcUwFCyvLo4dtoDLtsLMnl0pVjQEqMFZoq/LaXBBzyaoKnEtMoFtRbgp+\n" +
|
||||||
|
"bFOAsADhHppMCjeeIIm8xeV5WLdF/9PEof3ZeD1FFnTfgkQdHYFQWrkyTOJPPw46\n" +
|
||||||
|
"ZVpkgzspMcSZiLzFhDnyGRLhZtDq+3Wx0ie+kVmjKwnVXL9GjtZn1gvs2qvwgBmH\n" +
|
||||||
|
"ZAepd7FeDOLFHWqsXSPzMHU2TsrDTrBNjCzOUmFj3tX17+8KayMlJjw68sPCFhk/\n" +
|
||||||
|
"qTK6aPnJEjw+xh//m070kLBj9dEzADBa6CT6NUSbaoDzpsx7PHNfUMQwcdh0kCcK\n" +
|
||||||
|
"AU6lXrH42sJhgRGuKaOP+n5MTmKxAN6S449qLtrZOF1rfA3kAarIxm2LzcDIbuRX\n" +
|
||||||
|
"IYr2RjDZrVGhh5amU8kexrvD61X+jNZc1cbzyrBg0tQqH4iU00wa2gyU/sFdDSrb\n" +
|
||||||
|
"mSld9t0WxMhNdJ6A2dCq7XvjMORH2PUVwXG4xv3u/J6yX7W3ku3/yjf2x4K0VBOb\n" +
|
||||||
|
"g82Hi35k9i5UOiKxxcH0pSVTmk2oD+c1S4nfGYNmZNnb0WErJBsdRET7STCHt0kj\n" +
|
||||||
|
"CAKK4CXz9EM=\n" +
|
||||||
|
"-----END CERTIFICATE-----\n"
|
||||||
|
,
|
||||||
|
"-----BEGIN CERTIFICATE-----\n" +
|
||||||
|
"MIIGKDCCBBCgAwIBAgIJAKFO3vqQ8q6BMA0GCSqGSIb3DQEBCwUAMGYxCzAJBgNV\n" +
|
||||||
|
"BAYTAktHMQswCQYDVQQIEwJOQTEQMA4GA1UEBxMHQklTSEtFSzEVMBMGA1UEChMM\n" +
|
||||||
|
"T3BlblZQTi1URVNUMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21haW4w\n" +
|
||||||
|
"HhcNMTQxMDIyMjE1OTUyWhcNMjQxMDE5MjE1OTUyWjBmMQswCQYDVQQGEwJLRzEL\n" +
|
||||||
|
"MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t\n" +
|
||||||
|
"VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMIICIjANBgkq\n" +
|
||||||
|
"hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsJVPCqt3vtoDW2U0DII1QIh2Qs0dqh88\n" +
|
||||||
|
"8nivxAIm2LTq93e9fJhsq3P/UVYAYSeCIrekXypR0EQgSgcNTvGBMe20BoHO5yvb\n" +
|
||||||
|
"GjKPmjfLj6XRotCOGy8EDl/hLgRY9efiA8wsVfuvF2q/FblyJQPR/gPiDtTmUiqF\n" +
|
||||||
|
"qXa7AJmMrqFsnWppOuGd7Qc6aTsae4TF1e/gUTCTraa7NeHowDaKhdyFmEEnCYR5\n" +
|
||||||
|
"CeUsx2JlFWAH8PCrxBpHYbmGyvS0kH3+rQkaSM/Pzc2bS4ayHaOYRK5XsGq8XiNG\n" +
|
||||||
|
"KTTLnSaCdPeHsI+3xMHmEh+u5Og2DFGgvyD22gde6W2ezvEKCUDrzR7bsnYqqyUy\n" +
|
||||||
|
"n7LxnkPXGyvR52T06G8KzLKQRmDlPIXhzKMO07qkHmIonXTdF7YI1azwHpAtN4dS\n" +
|
||||||
|
"rUe1bvjiTSoEsQPfOAyvD0RMK/CBfgEZUzAB50e/IlbZ84c0DJfUMOm4xCyft1HF\n" +
|
||||||
|
"YpYeyCf5dxoIjweCPOoP426+aTXM7kqq0ieIr6YxnKV6OGGLKEY+VNZh1DS7enqV\n" +
|
||||||
|
"HP5i8eimyuUYPoQhbK9xtDGMgghnc6Hn8BldPMcvz98HdTEH4rBfA3yNuCxLSNow\n" +
|
||||||
|
"4jJuLjNXh2QeiUtWtkXja7ec+P7VqKTduJoRaX7cs+8E3ImigiRnvmK+npk7Nt1y\n" +
|
||||||
|
"YE9hBRhSoLsCAwEAAaOB2DCB1TAdBgNVHQ4EFgQUK0DlyX319JY46S/jL9lAZMmO\n" +
|
||||||
|
"BZswgZgGA1UdIwSBkDCBjYAUK0DlyX319JY46S/jL9lAZMmOBZuhaqRoMGYxCzAJ\n" +
|
||||||
|
"BgNVBAYTAktHMQswCQYDVQQIEwJOQTEQMA4GA1UEBxMHQklTSEtFSzEVMBMGA1UE\n" +
|
||||||
|
"ChMMT3BlblZQTi1URVNUMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21h\n" +
|
||||||
|
"aW6CCQChTt76kPKugTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG\n" +
|
||||||
|
"9w0BAQsFAAOCAgEABc77f4C4P8fIS+V8qCJmVNSDU44UZBc+D+J6ZTgW8JeOHUIj\n" +
|
||||||
|
"Bh++XDg3gwat7pIWQ8AU5R7h+fpBI9n3dadyIsMHGwSogHY9Gw7di2RVtSFajEth\n" +
|
||||||
|
"rvrq0JbzpwoYedMh84sJ2qI/DGKW9/Is9+O52fR+3z3dY3gNRDPQ5675BQ5CQW9I\n" +
|
||||||
|
"AJgLOqzD8Q0qrXYi7HaEqzNx6p7RDTuhFgvTd+vS5d5+28Z5fm2umnq+GKHF8W5P\n" +
|
||||||
|
"ylp2Js119FTVO7brusAMKPe5emc7tC2ov8OFFemQvfHR41PLryap2VD81IOgmt/J\n" +
|
||||||
|
"kX/j/y5KGux5HZ3lxXqdJbKcAq4NKYQT0mCkRD4l6szaCEJ+k0SiM9DdTcBDefhR\n" +
|
||||||
|
"9q+pCOyMh7d8QjQ1075mF7T+PGkZQUW1DUjEfrZhICnKgq+iEoUmM0Ee5WtRqcnu\n" +
|
||||||
|
"5BTGQ2mSfc6rV+Vr+eYXqcg7Nxb3vFXYSTod1UhefonVqwdmyJ2sC79zp36Tbo2+\n" +
|
||||||
|
"65NW2WJK7KzPUyOJU0U9bcu0utvDOvGWmG+aHbymJgcoFzvZmlXqMXn97pSFn4jV\n" +
|
||||||
|
"y3SLRgJXOw1QLXL2Y5abcuoBVr4gCOxxk2vBeVxOMRXNqSWZOFIF1bu/PxuDA+Sa\n" +
|
||||||
|
"hEi44aHbPXt9opdssz/hdGfd8Wo7vEJrbg7c6zR6C/Akav1Rzy9oohIdgOw=\n" +
|
||||||
|
"-----END CERTIFICATE-----\n"};
|
||||||
|
private final ExternalCertificateProvider.Stub mBinder = new ExternalCertificateProvider.Stub() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSignedData(String alias, byte[] data) throws RemoteException {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// This is more or less code that has been just modified long enough that it works
|
||||||
|
// Don't take it as good example how to get a Privatekey
|
||||||
|
StringReader keyreader = new StringReader(certchain + pemkey);
|
||||||
|
PEMParser pemparser = new PEMParser(keyreader);
|
||||||
|
|
||||||
|
PEMKeyPair bcKeyPair = (PEMKeyPair) pemparser.readObject();
|
||||||
|
|
||||||
|
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bcKeyPair.getPrivateKeyInfo().getEncoded());
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||||
|
PrivateKey key = kf.generatePrivate(keySpec);
|
||||||
|
|
||||||
|
// The actual signing
|
||||||
|
|
||||||
|
Cipher signer;
|
||||||
|
signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
|
||||||
|
|
||||||
|
|
||||||
|
signer.init(Cipher.ENCRYPT_MODE, key);
|
||||||
|
|
||||||
|
byte[] signed_bytes = signer.doFinal(data);
|
||||||
|
return signed_bytes;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (NoSuchPaddingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalBlockSizeException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (BadPaddingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
// Something failed, return null
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getCertificateChain(String alias) throws RemoteException {
|
||||||
|
|
||||||
|
return certchain;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
* Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.blinkt.externalcertprovider;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.security.KeyChain;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class SelectCertificateActivity extends Activity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
findViewById(R.id.nicecert).setOnClickListener((v) ->
|
||||||
|
{
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.putExtra(KeyChain.EXTRA_KEY_ALIAS, "mynicecert");
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0"/>
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1"/>
|
||||||
|
</vector>
|
@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout android:orientation="vertical"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".SelectCertificateActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="This demo app has no sophisticated selection!"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/nicecert"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Send mynicecert as string back to app"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
tlsexternalcertprovider/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 4.9 KiB |
BIN
tlsexternalcertprovider/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 15 KiB |
8
tlsexternalcertprovider/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">ExternalCertProvider</string>
|
||||||
|
</resources>
|
10
tlsexternalcertprovider/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</resources>
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2018 Arne Schwabe
|
||||||
|
* Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.blinkt.externalcertprovider;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|