├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── fidesmo
│ │ │ │ └── ble
│ │ │ │ └── client
│ │ │ │ ├── models
│ │ │ │ ├── CardBatch.java
│ │ │ │ ├── CardOperation.java
│ │ │ │ ├── CardInfo.java
│ │ │ │ └── Capabilities.java
│ │ │ │ ├── Utils.java
│ │ │ │ ├── BleCard.java
│ │ │ │ ├── apdu
│ │ │ │ └── CardInfoClient.java
│ │ │ │ ├── MainActivity.java
│ │ │ │ └── BlePeripheralService.java
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── fidesmo
│ │ └── ble
│ │ └── ApplicationTest.java
├── lint.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── images
├── NFC-traces.png
├── card-found.png
├── info-obtained.jpg
└── scan-started.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── LICENSE
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/images/NFC-traces.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/images/NFC-traces.png
--------------------------------------------------------------------------------
/images/card-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/images/card-found.png
--------------------------------------------------------------------------------
/images/info-obtained.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/images/info-obtained.jpg
--------------------------------------------------------------------------------
/images/scan-started.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/images/scan-started.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | APDU over BLE demo application
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fidesmo/apdu-over-ble-android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Aug 14 12:55:06 CEST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 |
4 | # sbt specific
5 | .cache
6 | .history
7 | .lib/
8 | dist/*
9 | target/
10 | lib_managed/
11 | src_managed/
12 | project/boot/
13 | project/plugins/project/
14 |
15 | # Scala-IDE specific
16 | .scala_dependencies
17 | .worksheet
18 | *.iml
19 | .gradle
20 | /local.properties
21 | .DS_Store
22 | /build
23 | /.idea/
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/fidesmo/ble/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/models/CardBatch.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client.models;
2 |
3 | public class CardBatch {
4 | private final long issuer;
5 | private final int batchId;
6 |
7 | public CardBatch(long issuer, int batchId) {
8 | this.issuer = issuer;
9 | this.batchId = batchId;
10 | }
11 |
12 | public long getIssuer() {
13 | return issuer;
14 | }
15 |
16 | public int getBatchId() {
17 | return batchId;
18 | }
19 |
20 | @Override
21 | public String toString() {
22 | return "CardBatch{" +
23 | "issuer=" + issuer +
24 | ", batchId=" + batchId +
25 | '}';
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/sergeykhruschak/workspace/tools/android-sdk-macosx/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/models/CardOperation.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client.models;
2 |
3 | import com.fidesmo.ble.client.Utils;
4 |
5 | public class CardOperation {
6 | private final long id;
7 | private final byte[] request;
8 | private byte[] response;
9 |
10 | public CardOperation(long id, byte[] request) {
11 | this.id = id;
12 | this.request = request;
13 | }
14 |
15 | public long getId() {
16 | return id;
17 | }
18 |
19 | public byte[] getRequest() {
20 | return request;
21 | }
22 |
23 | public byte[] getResponse() {
24 | return response;
25 | }
26 |
27 | public void setResponse(byte[] response) {
28 | this.response = response;
29 | }
30 |
31 | @Override
32 | public String toString() {
33 | return "CardOperation{" +
34 | "id=" + id +
35 | ", request=" + Utils.encodeHex(request) +
36 | ", response=" + (response != null ? Utils.encodeHex(response) : null) +
37 | '}';
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Fidesmo AB
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/models/CardInfo.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client.models;
2 |
3 | import java.util.Arrays;
4 |
5 | public class CardInfo {
6 | private final byte[] iin;
7 | private final byte[] cin;
8 | private final byte[] isdAid;
9 | private final CardBatch batch;
10 | private final Capabilities capabilities;
11 |
12 | public CardInfo(byte[] iin, byte[] cin, byte[] isdAid, CardBatch batch, Capabilities capabilities) {
13 | this.iin = iin;
14 | this.cin = cin;
15 | this.isdAid = isdAid;
16 | this.batch = batch;
17 | this.capabilities = capabilities;
18 | }
19 |
20 | public byte[] getIin() {
21 | return iin;
22 | }
23 |
24 | public byte[] getCin() {
25 | return cin;
26 | }
27 |
28 | public byte[] getIsdAid() {
29 | return isdAid;
30 | }
31 |
32 | public CardBatch getBatch() {
33 | return batch;
34 | }
35 |
36 | public Capabilities getCapabilities() {
37 | return capabilities;
38 | }
39 |
40 | @Override
41 | public String toString() {
42 | return "CardInfo{" +
43 | "iin=" + Arrays.toString(iin) +
44 | ", cin=" + Arrays.toString(cin) +
45 | ", isdAid=" + Arrays.toString(isdAid) +
46 | ", batch=" + batch +
47 | ", capabilities=" + capabilities +
48 | '}';
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | buildToolsVersion '28.0.3'
6 |
7 | defaultConfig {
8 | applicationId "com.fidesmo.ble.client"
9 | minSdkVersion 21
10 | targetSdkVersion 28
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 |
21 | lintOptions {
22 | disable 'Registered' //Ugly fix #2: Travis checks for the registered activities pre-compilation.
23 | if (project.hasProperty('strictCheck')) {
24 | warningsAsErrors = strictCheck
25 | }
26 | textReport true
27 | }
28 |
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 | }
34 |
35 | repositories {
36 | maven {
37 | url 'http://releases.marmeladburk.fidesmo.com'
38 | }
39 | maven {
40 | url "https://s3.amazonaws.com/repo.commonsware.com"
41 | }
42 | maven {
43 | url "http://dl.bintray.com/fidesmo/maven"
44 | }
45 | jcenter()
46 | }
47 |
48 | dependencies {
49 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
50 | testImplementation 'junit:junit:4.12'
51 | implementation 'com.android.support:appcompat-v7:28.0.0'
52 | implementation 'io.reactivex:rxjava:1.0.13'
53 | implementation 'com.fidesmo:ble-client-android:0.1.24@aar'
54 | implementation 'com.fidesmo:nordpol-android:0.1.23@aar'
55 | implementation 'com.fidesmo:nordpol-core:0.1.23'
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
23 |
24 |
31 |
32 |
37 |
38 |
39 |
40 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/models/Capabilities.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client.models;
2 |
3 | public class Capabilities {
4 | private final long platformVersion;
5 | private final Integer mifareType;
6 | private final Integer uidSize;
7 | private final Integer jcVersion;
8 | private final Integer osTypeVersion;
9 | private final Integer globalPlatformVersion;
10 |
11 | public Capabilities(long platformVersion, Integer mifareType, Integer uidSize, Integer jcVersion, Integer osTypeVersion, Integer globalPlatformVersion) {
12 | this.platformVersion = platformVersion;
13 | this.mifareType = mifareType;
14 | this.uidSize = uidSize;
15 | this.jcVersion = jcVersion;
16 | this.osTypeVersion = osTypeVersion;
17 | this.globalPlatformVersion = globalPlatformVersion;
18 | }
19 |
20 | public long getPlatformVersion() {
21 | return platformVersion;
22 | }
23 |
24 | public Integer getMifareType() {
25 | return mifareType;
26 | }
27 |
28 | public Integer getUidSize() {
29 | return uidSize;
30 | }
31 |
32 | public Integer getJcVersion() {
33 | return jcVersion;
34 | }
35 |
36 | public Integer getOsTypeVersion() {
37 | return osTypeVersion;
38 | }
39 |
40 | public Integer getGlobalPlatformVersion() {
41 | return globalPlatformVersion;
42 | }
43 |
44 | @Override
45 | public String toString() {
46 | final StringBuffer sb = new StringBuffer("Capabilities{");
47 | sb.append("platformVersion=").append(platformVersion);
48 | sb.append(", mifareType=").append(mifareType);
49 | sb.append(", uidSize=").append(uidSize);
50 | sb.append(", jcVersion=").append(jcVersion);
51 | sb.append(", osTypeVersion=").append(osTypeVersion);
52 | sb.append(", globalPlatformVersion=").append(globalPlatformVersion);
53 | sb.append('}');
54 | return sb.toString();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/Utils.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.Collections;
7 | import java.util.List;
8 | import java.util.Locale;
9 |
10 | public class Utils {
11 | final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
12 |
13 | public static byte[] decodeHex(String hexString) {
14 | if ((hexString.length() & 0x01) != 0) {
15 | throw new IllegalArgumentException("Odd number of characters.");
16 | }
17 | char[] hexChars = hexString.toUpperCase(Locale.ROOT).toCharArray();
18 | byte[] result = new byte[hexChars.length / 2];
19 | for (int i = 0; i < hexChars.length; i += 2) {
20 | result[i / 2] = (byte) (Arrays.binarySearch(hexArray, hexChars[i]) * 16 +
21 | Arrays.binarySearch(hexArray, hexChars[i + 1]));
22 | }
23 | return result;
24 | }
25 |
26 | public static String encodeHex(byte[] bytes) {
27 | char[] hexChars = new char[bytes.length * 2];
28 | for ( int i = 0; i < bytes.length; i++ ) {
29 | int v = bytes[i] & 0xFF;
30 | hexChars[i * 2] = hexArray[v >>> 4];
31 | hexChars[i * 2 + 1] = hexArray[v & 0x0F];
32 | }
33 | return new String(hexChars);
34 | }
35 |
36 | public static byte[] toApduSequence(List commands) {
37 | final int apduLenHeader = 2;
38 | final int apduNumberHeader = 2;
39 |
40 | int totalLen = apduNumberHeader;
41 | for (byte[] c: commands) {
42 | totalLen += c.length + apduLenHeader; // apdu len header
43 | }
44 |
45 | ByteBuffer result = ByteBuffer.allocate(totalLen);
46 | result.putShort((short)commands.size());
47 |
48 | for (byte[] c: commands) {
49 | result.putShort((short)c.length);
50 | result.put(c);
51 | }
52 |
53 | return result.array();
54 | }
55 |
56 | public static List fromApduSequence(byte[] responses) {
57 | ByteBuffer bf = ByteBuffer.wrap(responses);
58 |
59 | int count = bf.getShort();
60 |
61 | if (count > 100) {
62 | throw new IllegalArgumentException("Number of APDUs cannot exceed 100");
63 | }
64 |
65 | List result = new ArrayList<>(count);
66 |
67 | for (int i = 0; i < count; i++) {
68 | int len = bf.getShort();
69 |
70 | if (len > 255) {
71 | throw new IllegalArgumentException("APDU cannot be bigger than 255");
72 | }
73 |
74 | byte[] apdu = new byte[len];
75 | bf.get(apdu);
76 |
77 | result.add(apdu);
78 | }
79 |
80 | return result;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android BLE client-server prototype
2 |
3 | Prototype implementing the server and client sides of the APDU over Bluetooth Low Energy transport protocol. The specification is published in this repository: [https://github.com/fidesmo/apdu-over-ble](https://github.com/fidesmo/apdu-over-ble)
4 |
5 | ## Usage
6 |
7 | This application is a proof of concept for demonstrating that it is possible to transmit APDUs over BLE. The application should be installed in two Android terminals to work
8 | correctly. One of these Android terminals will be the [server](#Server) (in
9 | charge of all the NFC communication), and the other will be the [client](#Client) sending the commands to the card connected to the server.
10 |
11 | To fully test the Client --> Server --> Secure Element connection, it is necessary to have a contactless device, like for example a [Fidesmo Card](https://developer.fidesmo.com/fidesmocard).
12 |
13 | ## Server
14 |
15 | For activating the server mode you just need to install and start the
16 | application on it. If phone supports BLE peripheral mode – server will be started automatically.
17 | Please bear in mind that not all Android phones can be BLE servers. The phone needs to have an Android
18 | version >= 6.0 (API level >= 23) and NFC capabilities.
19 |
20 | For this to work correctly, please be sure to attach a contactless card to the
21 | phone after establishing the connection. Server will print a message: `Please attach the card to the phone` which indicates that a
22 | card can be attached. You will know that the card is detected when the text `Card attached` appears on the screen.
23 |
24 |
25 |
26 | When the phone acting as the client starts the communication, you will see all
27 | the traces of the NFC communication in the server:
28 |
29 |
30 |
31 | ## Client
32 |
33 | The prototype can run as Client on phones having an Android version >= 5.0 (API level >= 21).
34 | For a phone to work as a BLE client, you need to install the app in it, and then follow the following instructions:
35 |
36 | 1. Click the "Scan" button at the bottom. This will start scanning for BLE Servers advertising:
37 |
38 |
39 |
40 | 2. The client will automatically discover the peripheral device (server). If the server is this same application running in server mode, the server will ask the user to attach the NFC card to it.
41 | If the card is a Fidesmo card (link to https://developer.fidesmo.com/fidesmocard) the card identifiers are read by the server and transmitted to the client, which displays them.
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/BleCard.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client;
2 |
3 | import android.Manifest;
4 | import android.annotation.TargetApi;
5 | import android.bluetooth.BluetoothDevice;
6 | import android.content.Context;
7 | import android.os.Build;
8 | import android.support.annotation.RequiresPermission;
9 | import com.fidesmo.ble.client.protocol.SimplePacketFragmenter;
10 | import nordpol.IsoCard;
11 | import nordpol.OnCardErrorListener;
12 |
13 | import java.io.Closeable;
14 | import java.io.IOException;
15 | import java.util.Collections;
16 | import java.util.List;
17 | import java.util.UUID;
18 | import java.util.concurrent.CopyOnWriteArrayList;
19 | import java.util.concurrent.CountDownLatch;
20 | import java.util.concurrent.TimeUnit;
21 | import static com.fidesmo.ble.client.Utils.*;
22 | import static com.fidesmo.ble.client.Utils.fromApduSequence;
23 |
24 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
25 | public class BleCard implements IsoCard, Closeable {
26 | private static final String TAG = BleCard.class.getName();
27 |
28 | public static final UUID APDU_SERVICE_UUID = UUID.fromString("8e790d52-bb90-4967-a4a5-3f21aa9e05eb");
29 | public static final UUID APDU_WRITE_CHARACTERISTIC_UUID = UUID.fromString("8e79ecae-bb90-4967-a4a5-3f21aa9e05eb");
30 | public static final UUID APDU_CONVERSATION_FINISHED_CHARACTERISTIC_UUID = UUID.fromString("8e798746-bb90-4967-a4a5-3f21aa9e05eb");
31 | public static final UUID APDU_RESPONSE_READY_NOTIFY_CHARACTERISTIC_UUID = UUID.fromString("8e795e92-bb90-4967-a4a5-3f21aa9e05eb");
32 | public static final UUID APDU_READ_CHARACTERISTIC_UUID = UUID.fromString("8e7927a7-bb90-4967-a4a5-3f21aa9e05eb");
33 | public static final UUID APDU_MAX_MEMORY_FOR_APDU_PROCESSING = UUID.fromString("8e79e13b-bb90-4967-a4a5-3f21aa9e05eb");
34 |
35 | private BleGattServiceClient gattClient;
36 |
37 | private int timeout = 120000;
38 | private int transceiveLength = 512;
39 | private boolean connected = false;
40 |
41 | private List errorListeners = new CopyOnWriteArrayList();
42 |
43 | @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
44 | public BleCard(Context context, BluetoothDevice device) {
45 | gattClient = new BleGattServiceClient(context,
46 | device,
47 | SimplePacketFragmenter.factory(),
48 | transceiveLength);
49 | }
50 |
51 | @Override
52 | public void addOnCardErrorListener(OnCardErrorListener onCardErrorListener) {
53 | errorListeners.add(onCardErrorListener);
54 | }
55 |
56 | @Override
57 | public void removeOnCardErrorListener(OnCardErrorListener onCardErrorListener) {
58 | errorListeners.remove(onCardErrorListener);
59 | }
60 |
61 | @Override
62 | public void close() throws IOException {
63 | try {
64 | gattClient.send(new byte[]{0,0,0,0}, APDU_SERVICE_UUID, APDU_CONVERSATION_FINISHED_CHARACTERISTIC_UUID).get();
65 | } catch (Exception e) {
66 | throw new IOException(e);
67 | }
68 |
69 | gattClient.close();
70 | }
71 |
72 | @Override
73 | public void connect() throws IOException {
74 |
75 | try {
76 | final CountDownLatch connectionLatch = new CountDownLatch(1);
77 |
78 | gattClient.connect(false, new BleConnectionListener() {
79 | public void connectionEstablished() {
80 | connectionLatch.countDown();
81 | }
82 | });
83 |
84 | connectionLatch.await(timeout, TimeUnit.MILLISECONDS);
85 | } catch (InterruptedException e) {
86 | throw new IOException(e);
87 | }
88 |
89 | readMaxApduSequenceSize();
90 | }
91 |
92 | /**
93 | * Obtains max APDU sequence size that can fit into device memory.
94 | */
95 | private void readMaxApduSequenceSize() {
96 | try {
97 | byte[] buffer = gattClient.read(APDU_SERVICE_UUID, APDU_MAX_MEMORY_FOR_APDU_PROCESSING).get(2, TimeUnit.MINUTES);
98 | transceiveLength = BleUtils.unpackInt4(buffer, 0);
99 | } catch (Exception ex) {
100 | transceiveLength = Integer.MAX_VALUE;
101 | }
102 | }
103 |
104 | @Override
105 | public int getMaxTransceiveLength() throws IOException {
106 | return transceiveLength;
107 | }
108 |
109 | @Override
110 | public int getTimeout() {
111 | return timeout;
112 | }
113 |
114 | @Override
115 | public boolean isConnected() {
116 | return connected;
117 | }
118 |
119 | @Override
120 | public void setTimeout(int timeout) {
121 | this.timeout = timeout;
122 | }
123 |
124 | @Override
125 | public byte[] transceive(byte[] command) throws IOException {
126 | try {
127 | List apduResponses = transceive(Collections.singletonList(command));
128 |
129 | if (apduResponses.size() != 1) {
130 | throw new IllegalArgumentException("Only a single response is expected");
131 | }
132 |
133 | return apduResponses.get(0);
134 | } catch (Exception e) {
135 | throw new IOException(e);
136 | }
137 | }
138 |
139 | @Override
140 | public List transceive(List commands) throws IOException {
141 | try {
142 | byte[] apduSeq = toApduSequence(commands);
143 |
144 | byte[] response = gattClient.sendReceive(apduSeq,
145 | APDU_SERVICE_UUID,
146 | APDU_WRITE_CHARACTERISTIC_UUID,
147 | APDU_RESPONSE_READY_NOTIFY_CHARACTERISTIC_UUID,
148 | APDU_READ_CHARACTERISTIC_UUID
149 | ).get();
150 |
151 | return fromApduSequence(response);
152 | } catch (Exception e) {
153 | throw new IOException(e);
154 | }
155 | }
156 |
157 |
158 |
159 | }
160 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/apdu/CardInfoClient.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client.apdu;
2 |
3 | import com.fidesmo.ble.client.BleCard;
4 | import com.fidesmo.ble.client.Utils;
5 | import com.fidesmo.ble.client.models.Capabilities;
6 | import com.fidesmo.ble.client.models.CardBatch;
7 | import com.fidesmo.ble.client.models.CardInfo;
8 |
9 |
10 | import java.io.ByteArrayInputStream;
11 | import java.io.DataInputStream;
12 | import java.io.IOException;
13 | import java.nio.ByteBuffer;
14 | import java.util.Formatter;
15 |
16 | public class CardInfoClient {
17 | protected static byte[] selectIsd = Utils.decodeHex("00A4040000");
18 | private static final String PLATFORM_VERSION_AID = "A000000617020002000001";
19 | private static final String CARD_DATA_AID = "A000000617020002000002";
20 |
21 | public static final int BATCH_TLV_ID = 0x42;
22 | public static final int ISSUER_TLV_ID = 0x43;
23 |
24 | private final BleCard device;
25 |
26 | public CardInfoClient(BleCard device) {
27 | this.device = device;
28 | }
29 |
30 | /** Apdus that can be sent to the isd client
31 | */
32 | protected static byte[] getData(int tag) {
33 | return Utils.decodeHex(String.format("80CA%04X00", tag & 0xFFFF).toUpperCase());
34 | }
35 |
36 | protected static int tagIin = 0x42;
37 | protected static int tagCin = 0x45;
38 | protected static int tagFci = 0x6F;
39 | protected static int tagAid = 0x84;
40 |
41 | // card capabilities tags
42 | protected static final int tagPlatformVersion = 0x41;
43 | protected static final int tagMifareType = 0x42;
44 | protected static final int tagUidSize = 0x43;
45 | protected static final int tagJcVersion = 0x44;
46 | protected static final int tagOsTypeVersion = 0x45;
47 |
48 |
49 | public CardInfo getCardInfo() throws Exception {
50 | if(!device.isConnected()) {
51 | device.connect();
52 | }
53 |
54 | CardInfo cardInfo = transceiveCardInfo();
55 |
56 | device.close();
57 |
58 | return cardInfo;
59 | }
60 |
61 |
62 | /** Get a unique identifier for the card by quering IIN and CIN */
63 | private CardInfo transceiveCardInfo() throws Exception {
64 | byte[] aid = transceiveSelectIsd();
65 | byte[] iin = transceiveGetData(tagIin, "Query issuer identification number");
66 | byte[] cin = transceiveGetData(tagCin, "Query card image number");
67 | Capabilities capabilities = transceiveCapabilites();
68 | CardBatch batch = transceiveBatchInfo();
69 | return new CardInfo(iin, cin, aid, batch, capabilities);
70 | }
71 |
72 | private Capabilities transceiveCapabilites() throws Exception {
73 | String select = String.format("00A40400%02X%s00",
74 | PLATFORM_VERSION_AID.length()/2,
75 | PLATFORM_VERSION_AID).toUpperCase();
76 | int[] statusWords = {0x6A82, 0x9000};
77 | byte[] response = transceive(Utils.decodeHex(select), "Query platform version", statusWords);
78 | ByteBuffer buffer = ByteBuffer.wrap(response);
79 |
80 | long platformVersion = 0;
81 | Integer mifareType = null;
82 | Integer uidSize = null;
83 | Integer jcVersion = null;
84 | Integer osTypeVersion = null;
85 | Integer gpVersion = null;
86 |
87 | while (buffer.remaining() > 2) {
88 | int tag = getTlvTag(buffer);
89 | byte data[] = getTlvData(buffer);
90 |
91 | switch (tag) {
92 | case tagPlatformVersion:
93 | platformVersion = parseLong(data, data.length);
94 | break;
95 | case tagMifareType:
96 | mifareType = new Long(parseLong(data, data.length)).intValue();
97 | break;
98 | case tagUidSize:
99 | uidSize = new Long(parseLong(data, data.length)).intValue();
100 | break;
101 | case tagJcVersion:
102 | jcVersion = new Long(parseLong(data, data.length)).intValue();
103 | break;
104 | case tagOsTypeVersion:
105 | osTypeVersion = new Long(parseLong(data, data.length)).intValue();
106 | break;
107 | default:
108 | throw new Exception("Unexpected tag during transceive platform");
109 | }
110 | }
111 |
112 | // perform a select ISD to get global platform version
113 | ByteBuffer outer = ByteBuffer.wrap(transceive(selectIsd, "Select isd"));
114 | if (getTlvTag(outer) == tagFci) {
115 | ByteBuffer inner = searchForTlvInLv(outer, 0xA5);
116 | if (inner != null) {
117 | inner = searchForTlvInLv(inner, 0x73);
118 | if (inner != null) {
119 | inner = searchForTlvInLv(inner, 0x60);
120 | if (inner != null) {
121 | inner = searchForTlvInLv(inner, 0x06);
122 | if (inner != null) {
123 | int value = 0;
124 | byte[] oid = getTlvData(inner);
125 |
126 | // read global platform version from last 2 or 3 bytes
127 | for (int i = 7; i < oid.length; i ++) {
128 | value = (value << 8) + (oid[i] & 0xFF);
129 | }
130 | gpVersion = value;
131 | }
132 | }
133 | }
134 | }
135 | }
136 |
137 | return new Capabilities(platformVersion,
138 | mifareType,
139 | uidSize,
140 | jcVersion,
141 | osTypeVersion,
142 | gpVersion);
143 | }
144 |
145 | /** Query card data with getData command
146 | */
147 | private byte[] transceiveGetData(int tag, String msg) throws Exception {
148 | byte[] response = transceive(getData(tag), msg);
149 | ByteBuffer buffer = ByteBuffer.wrap(response);
150 |
151 | if (getTlvTag(buffer) != tag) {
152 | throw new Exception(String.format("Invalid IIN tlv tag 0x%4X", tag));
153 | }
154 |
155 | return getTlvData(buffer);
156 | }
157 |
158 | private byte[] transceiveSelectIsd() throws Exception {
159 | ByteBuffer buffer = ByteBuffer.wrap(transceive(selectIsd, "Select isd"));
160 |
161 | if (getTlvTag(buffer) == tagFci) {
162 | ByteBuffer inner = ByteBuffer.wrap(getTlvData(buffer));
163 | if (getTlvTag(inner) == tagAid) {
164 | return getTlvData(inner);
165 | } else {
166 | throw new Exception("No AID tag present in FCI");
167 | }
168 | } else {
169 | throw new Exception("No FCI tag present in select response");
170 | }
171 | }
172 |
173 | private CardBatch transceiveBatchInfo() throws Exception {
174 | String select = String.format("00A40400%02X%s00",
175 | CARD_DATA_AID.length()/2,
176 | CARD_DATA_AID).toUpperCase();
177 | byte[] response = transceive(Utils.decodeHex(select), "Query account id");
178 | ByteBuffer buffer = ByteBuffer.wrap(response);
179 |
180 | while (getTlvTag(buffer) != BATCH_TLV_ID) {
181 | getTlvData(buffer);
182 | }
183 |
184 | byte[] batchBytes = getTlvData(buffer);
185 | int batchId = (int) parseLong(batchBytes, batchBytes.length);
186 |
187 | buffer.rewind();
188 |
189 | while (getTlvTag(buffer) != ISSUER_TLV_ID) {
190 | getTlvData(buffer);
191 | }
192 |
193 | long issuerId = parseLong(getTlvData(buffer), 6);
194 |
195 | return new CardBatch(issuerId, batchId);
196 | }
197 |
198 | public static long parseLong(byte[] data, int positions) {
199 | DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
200 | long result = 0;
201 |
202 | if (data.length != positions) {
203 | throw new RuntimeException("Data must be " + positions + " bytes");
204 | }
205 |
206 | try {
207 | for (int i = positions - 1; i >= 0; i--) {
208 | result += ((long) is.readUnsignedByte() << (8 * i));
209 | }
210 | } catch (IOException ioex) {
211 | throw new RuntimeException(ioex);
212 | }
213 |
214 | return result;
215 | }
216 |
217 |
218 | protected byte[] transceive(byte[] command) throws IOException {
219 | return device.transceive(command);
220 | }
221 |
222 | /** Send command to SE and reads its response. The status word is checked against a list of
223 | * expected status words. This list might contain entire status words or only prefixes. If
224 | * an unexpected status word is encountered an exception containg the description is thrown.
225 | *
226 | * @param command APDU that should be sent
227 | * @param description Human readable description of the operation performed
228 | * @param statusWords List of accepted status words or status word prefixes
229 | * @return Apdu response received
230 | */
231 | protected byte[] transceive(byte[] command, String description, int[] statusWords) throws Exception {
232 | byte[] response = transceive(command);
233 |
234 | int status = statusWord(response);
235 | boolean success = false;
236 |
237 | for (int expected: statusWords) {
238 | success = (expected <= 0xFF && (status >> 8) == expected) || (expected == status);
239 | if (success) {
240 | break;
241 | }
242 | }
243 |
244 | if(!success) {
245 | Formatter formatter = new Formatter();
246 | formatter.format("%s failed with status word %04X, %d", description, status, status);
247 | throw new Exception(formatter.toString());
248 | }
249 | return response;
250 | }
251 |
252 | /** Transceive with 0x9000 as only acceptable status word
253 | * @param command APDU that should be sent
254 | * @param description Human readable description of the operation performed
255 | * @return APDU response received
256 | */
257 | private byte[] transceive(byte[] command, String description) throws Exception {
258 | int[] accepted = { 0x9000 };
259 | return transceive(command, description, accepted);
260 | }
261 |
262 | /** Assumes BER tag is preset at current buffer position and
263 | * extracts value */
264 | private int getTlvTag(ByteBuffer buffer) {
265 | int firstTagByte = buffer.get() & 0xFF;
266 | if ((firstTagByte & 0x1F) == 0x1F) {
267 | return (firstTagByte << 8) + (buffer.get() & 0xFF);
268 | } else {
269 | return firstTagByte;
270 | }
271 | }
272 |
273 | /** Assumes that BER length|value is present at current buffer
274 | * position and extracts value */
275 | private byte[] getTlvData(ByteBuffer buffer) {
276 | int length = buffer.get() & 0x7F; // this only works for length <= 127
277 | byte[] target = new byte[length];
278 | buffer.get(target);
279 | return target;
280 | }
281 |
282 | /** Assumes that non-primitive BER LV is present at the current buffer
283 | * position, and LV for given inner tag. */
284 | private ByteBuffer searchForTlvInLv(ByteBuffer istream, int innerTag) {
285 | ByteBuffer buffer = ByteBuffer.wrap(getTlvData(istream));
286 |
287 | while (buffer.remaining() > 0 && getTlvTag(buffer) != innerTag) {
288 | getTlvData(buffer);
289 | }
290 |
291 | if (buffer.remaining() == 0) {
292 | return null;
293 | } else {
294 | return buffer;
295 | }
296 | }
297 |
298 | /** Get status word from response.
299 | * @param response Response from which the status code is to be extracted
300 | * @return The status word as an integer
301 | */
302 | private int statusWord(byte[] response) {
303 | int r = (response[response.length - 1] & 0xFF) +
304 | ((response[response.length - 2] & 0xFF) << 8);
305 | return r;
306 | }
307 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client;
2 |
3 | import android.Manifest;
4 | import android.annotation.TargetApi;
5 | import android.bluetooth.BluetoothAdapter;
6 | import android.bluetooth.BluetoothDevice;
7 | import android.bluetooth.BluetoothManager;
8 | import android.content.BroadcastReceiver;
9 | import android.content.Context;
10 | import android.content.Intent;
11 | import android.content.IntentFilter;
12 | import android.content.pm.PackageManager;
13 | import android.nfc.NfcAdapter;
14 | import android.nfc.Tag;
15 | import android.os.Bundle;
16 | import android.support.v4.app.ActivityCompat;
17 | import android.support.v4.content.ContextCompat;
18 | import android.support.v4.content.LocalBroadcastManager;
19 | import android.support.v7.app.AppCompatActivity;
20 | import android.util.Log;
21 | import android.view.View;
22 | import android.widget.ScrollView;
23 | import android.widget.TextView;
24 | import android.widget.Toast;
25 | import com.fidesmo.ble.R;
26 | import com.fidesmo.ble.client.apdu.CardInfoClient;
27 | import com.fidesmo.ble.client.models.CardInfo;
28 | import com.fidesmo.ble.client.models.CardOperation;
29 | import nordpol.IsoCard;
30 | import nordpol.android.AndroidCard;
31 | import nordpol.android.OnDiscoveredTagListener;
32 | import nordpol.android.TagDispatcher;
33 | import nordpol.android.TagDispatcherBuilder;
34 |
35 | import java.io.IOException;
36 | import java.util.LinkedList;
37 | import java.util.List;
38 |
39 | import static com.fidesmo.ble.client.BleUtils.byteArrayToString;
40 |
41 | @TargetApi(23)
42 | public class MainActivity extends AppCompatActivity implements OnDiscoveredTagListener, BleDeviceListener, LogsConsumer {
43 | private String TAG = getClass().getSimpleName();
44 |
45 | final private int REQUEST_CODE_SCAN = 123;
46 | final private int REQUEST_CODE_ADVERT = 124;
47 |
48 | private BleDeviceScanner deviceScanner =
49 | BleDeviceScanner.singleServiceScanner(this, BleCard.APDU_SERVICE_UUID, this, this);
50 |
51 | private TagDispatcher nfcTagDispatcher;
52 |
53 | private IsoCard nfcCard;
54 |
55 | private LinkedList pendingOperations = new LinkedList<>();
56 |
57 | private BroadcastReceiver apduReceiver = new BroadcastReceiver() {
58 | @Override
59 | public void onReceive(Context context, Intent intent) {
60 | final byte[] data = intent.getByteArrayExtra("apdu");
61 | final long requestId = intent.getLongExtra("id", -1L);
62 |
63 | pendingOperations.offerLast(new CardOperation(requestId, data));
64 |
65 | processPendingCardOperations();
66 | }
67 | };
68 |
69 | protected void onCreate(Bundle savedInstanceState) {
70 | super.onCreate(savedInstanceState);
71 | setContentView(R.layout.activity_main);
72 |
73 | // Use this check to determine whether BLE is supported on the device. Then you can selectively disable BLE-related features.
74 | if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
75 | Toast.makeText(this, "BLE is not supported on this device", Toast.LENGTH_SHORT).show();
76 | finish();
77 | } else {
78 | LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
79 |
80 | IntentFilter logFilter = new IntentFilter(BlePeripheralService.LOG);
81 | IntentFilter apduFilter = new IntentFilter(BlePeripheralService.BLE_APDU);
82 | IntentFilter conversationFinishFilter = new IntentFilter(BlePeripheralService.CONVERSATION_FINISHED);
83 |
84 | broadcastManager.registerReceiver(new BroadcastReceiver() {
85 | @Override
86 | public void onReceive(Context context, Intent intent) {
87 | final String message = intent.getExtras().getString("data");
88 | log("SERVICE", message);
89 | }
90 | }, logFilter);
91 |
92 | broadcastManager.registerReceiver(apduReceiver, apduFilter);
93 |
94 | broadcastManager.registerReceiver(new BroadcastReceiver() {
95 | @Override
96 | public void onReceive(Context context, Intent intent) {
97 | log("Conversation finished");
98 | pendingOperations.clear();
99 | }
100 | }, conversationFinishFilter);
101 |
102 | askForBtDevicePermissionsAndFireAction(REQUEST_CODE_ADVERT);
103 | }
104 |
105 | nfcTagDispatcher = new TagDispatcherBuilder(this, this).build();
106 | }
107 |
108 |
109 | @Override
110 | public void tagDiscovered(Tag tag) {
111 | try {
112 | nfcCard = AndroidCard.get(tag);
113 |
114 | if (pendingOperations.isEmpty()) {
115 | log("NFC Card attached. Awaiting for connection");
116 | } else {
117 | log("NFC Card attached. Processing pending operations");
118 | }
119 |
120 | processPendingCardOperations();
121 | } catch (IOException e) {
122 | e.printStackTrace();
123 | }
124 | }
125 |
126 | @Override
127 | protected void onDestroy() {
128 | super.onDestroy();
129 | deviceScanner.stopScan();
130 | }
131 |
132 | @Override
133 | protected void onPause() {
134 | super.onPause();
135 | deviceScanner.stopScan();
136 | nfcTagDispatcher.disableExclusiveNfc();
137 | }
138 |
139 | @Override
140 | protected void onResume() {
141 | super.onResume();
142 | nfcTagDispatcher.enableExclusiveNfc();
143 | }
144 |
145 | @Override
146 | public void onNewIntent(Intent intent) {
147 | Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
148 | nfcTagDispatcher.interceptIntent(intent);
149 | }
150 |
151 | public void onDiscoveryClicked(View v) {
152 | if(!deviceScanner.isActive()) {
153 | clearLog();
154 | log("Discovery clicked");
155 | askForBtDevicePermissionsAndFireAction(REQUEST_CODE_SCAN);
156 | } else {
157 | deviceScanner.stopScan();
158 | }
159 | }
160 |
161 | private void askForBtDevicePermissionsAndFireAction(int requestCode) {
162 | int hasWriteBtPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN);
163 | int hasWriteLocPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION);
164 |
165 | if (hasWriteBtPermission != PackageManager.PERMISSION_GRANTED || hasWriteLocPermission != PackageManager.PERMISSION_GRANTED ) {
166 |
167 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.BLUETOOTH_ADMIN)) {
168 | Toast.makeText(MainActivity.this, "App needs bluetooth to work", Toast.LENGTH_SHORT).show();
169 | } else {
170 | ActivityCompat.requestPermissions(this,
171 | new String[] {Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_COARSE_LOCATION},
172 | requestCode);
173 |
174 | }
175 |
176 | return ;
177 | }
178 |
179 | enableBluetoothAndAction(requestCode);
180 | }
181 |
182 | @Override
183 | public void deviceDiscovered(BluetoothDevice bluetoothDevice) {
184 | log("BLE device discovered. Obtaining card information");
185 |
186 | CardInfoClient client = new CardInfoClient(new BleCard(this, bluetoothDevice));
187 |
188 | try {
189 | CardInfo cardInfo = client.getCardInfo();
190 |
191 | log("Card info: IIN: " + Utils.encodeHex(cardInfo.getIin()) +
192 | ", platform: " + cardInfo.getCapabilities().getPlatformVersion() +
193 | ", cin: " + Utils.encodeHex((cardInfo.getCin()))
194 | );
195 |
196 | } catch (Exception e) {
197 | log("Failed to get remote card information");
198 | e.printStackTrace();
199 | }
200 | }
201 |
202 | @Override
203 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
204 | switch (requestCode) {
205 | case REQUEST_CODE_SCAN:
206 | case REQUEST_CODE_ADVERT:
207 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED &&
208 | grantResults[1] == PackageManager.PERMISSION_GRANTED &&
209 | grantResults[2] == PackageManager.PERMISSION_GRANTED) {
210 |
211 | enableBluetoothAndAction(requestCode);
212 | } else {
213 | Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show();
214 | }
215 |
216 | break;
217 |
218 | default:
219 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
220 | }
221 | }
222 |
223 | private void enableBluetoothAndAction(int requestCode) {
224 | BluetoothManager btManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
225 | BluetoothAdapter btAdapter = btManager.getAdapter();
226 |
227 | if (btAdapter == null || !btAdapter.isEnabled()) {
228 | Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
229 | startActivityForResult(enableBtIntent, requestCode);
230 | return;
231 | }
232 |
233 | onActivityResult(requestCode, 0, null);
234 | }
235 |
236 | @Override
237 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
238 | switch (requestCode) {
239 | case REQUEST_CODE_ADVERT:
240 | startAdvertise();
241 | break;
242 | case REQUEST_CODE_SCAN:
243 | startScan();
244 | break;
245 | default:
246 | super.onActivityResult(requestCode, resultCode, data);
247 | }
248 | }
249 |
250 | private void startScan() {
251 | deviceScanner.startDiscovery();
252 | }
253 |
254 | private void startAdvertise() {
255 | Intent intent = new Intent(this, BlePeripheralService.class);
256 | startService(intent);
257 |
258 | Intent localIntent = new Intent(BlePeripheralService.ACTION).putExtra("cmd", BlePeripheralService.CMD_LOGS);
259 | LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
260 | }
261 |
262 | private void processPendingCardOperations() {
263 | if ( nfcCard == null) {
264 | log("Please attach card to the phone, operations pending:" + pendingOperations.size());
265 | return;
266 | }
267 |
268 | CardOperation operation = pendingOperations.poll();
269 |
270 | while (operation != null && nfcCard != null) {
271 | try {
272 |
273 | if(!nfcCard.isConnected()) {
274 | nfcCard.connect();
275 | }
276 |
277 | Log.i(TAG, "Trying to transcieve data to a card: " + byteArrayToString(operation.getRequest()));
278 |
279 | List result = nfcCard.transceive(Utils.fromApduSequence(operation.getRequest()));
280 | operation.setResponse(Utils.toApduSequence(result));
281 |
282 | sendResponse(operation);
283 |
284 | operation = pendingOperations.poll();
285 |
286 | } catch (IOException e) {
287 | log("NFC card disconnected: " + e.getMessage());
288 | Log.w(TAG, e);
289 | pendingOperations.offerFirst(operation);
290 | nfcCard = null;
291 | }
292 | }
293 | }
294 |
295 | private void sendResponse(CardOperation op) {
296 | log("RESPONSE: ", op.toString());
297 |
298 | Intent localIntent = new Intent(BlePeripheralService.ACTION)
299 | .putExtra("cmd", BlePeripheralService.CMD_SE_RESPONSE)
300 | .putExtra("id", op.getId())
301 | .putExtra("apdu-response", op.getResponse());
302 |
303 | LocalBroadcastManager.getInstance(MainActivity.this).sendBroadcast(localIntent);
304 | }
305 |
306 | private void log(final String prefix, final String message) {
307 | runOnUiThread(new Runnable() { public void run() {
308 | TextView tv = (TextView) findViewById(R.id.outputView);
309 | tv.append(prefix + ": " + message + "\n");
310 | Log.i(TAG, message);
311 |
312 | //Scroll to the bottom of the view once the text has been appended.
313 | final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
314 | scrollView.post(new Runnable() {
315 | public void run() {
316 | scrollView.fullScroll(View.FOCUS_DOWN);
317 | }
318 | });
319 | }
320 | });
321 | }
322 |
323 | private void clearLog() {
324 | ((TextView)findViewById(R.id.outputView)).setText("");
325 | }
326 |
327 | public void log(String message) {
328 | log("ACTIVITY", message);
329 | }
330 |
331 | }
332 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fidesmo/ble/client/BlePeripheralService.java:
--------------------------------------------------------------------------------
1 | package com.fidesmo.ble.client;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Service;
5 | import android.bluetooth.*;
6 | import android.bluetooth.le.AdvertiseCallback;
7 | import android.bluetooth.le.AdvertiseData;
8 | import android.bluetooth.le.AdvertiseSettings;
9 | import android.bluetooth.le.BluetoothLeAdvertiser;
10 | import android.content.BroadcastReceiver;
11 | import android.content.Context;
12 | import android.content.Intent;
13 | import android.content.IntentFilter;
14 | import android.os.Binder;
15 | import android.os.IBinder;
16 | import android.os.ParcelUuid;
17 | import android.support.v4.content.LocalBroadcastManager;
18 | import android.util.Log;
19 | import android.widget.Toast;
20 | import com.fidesmo.ble.client.protocol.FragmentationProtocol;
21 | import com.fidesmo.ble.client.protocol.PacketDefragmenter;
22 | import com.fidesmo.ble.client.protocol.PacketFragmenter;
23 | import com.fidesmo.ble.client.protocol.SimplePacketFragmenter;
24 |
25 | import java.util.LinkedList;
26 | import java.util.UUID;
27 | import java.util.concurrent.Callable;
28 | import java.util.concurrent.atomic.AtomicLong;
29 |
30 | import static com.fidesmo.ble.client.BleCard.APDU_CONVERSATION_FINISHED_CHARACTERISTIC_UUID;
31 | import static com.fidesmo.ble.client.BleUtils.*;
32 | import static android.bluetooth.BluetoothProfile.GATT_SERVER;
33 |
34 | /**
35 | * Implementation of BLE server that receives APDUs over BLE and passes them to the MainActivity using intent.
36 | * Responses from a card then received back and passed to the client.
37 | */
38 | @TargetApi(21)
39 | public class BlePeripheralService extends Service {
40 |
41 | public static final String ACTION = "com.fidesmo.ble.client.BlePeripheralService.ACTION";
42 | public static final String LOG = "com.fidesmo.ble.client.BlePeripheralService.LOG";
43 | public static final String BLE_APDU = "com.fidesmo.ble.client.BlePeripheralService.BLE_APDU";
44 | public static final String CONVERSATION_FINISHED = "com.fidesmo.ble.client.BlePeripheralService.CONVERSATION_FINISHED";
45 | // Client Characteristic Configuration Descriptor (CCCD): https://www.bluetooth.com/specifications/gatt/descriptors
46 | public static final String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
47 |
48 | public static final String CMD_LOGS = "LOGS";
49 | public static final String CMD_SE_RESPONSE = "SE_RESPONSE";
50 | public static final String CMD_STOP = "STOP";
51 | public static final String NFC_RESPONSE = "NFC_RESPONSE";
52 |
53 | public static final int MAX_LOG_BUFFER = 200;
54 | public static final int MAX_MEMORY = 512;
55 |
56 | private final IBinder binder = new LocalBinder();
57 |
58 | private BluetoothManager btManager;
59 | private BluetoothAdapter btAdapter;
60 | private BluetoothGattServerCallback gattServerCallback;
61 | private BluetoothGattServer gattServer;
62 |
63 | private AdvertiseSettings advertSettings;
64 | private AdvertiseCallback advertCallback;
65 |
66 | private BluetoothGattCharacteristic readNotifyCharacteristic;
67 |
68 | private LinkedList messagesList = new LinkedList<>();
69 |
70 | private FragmentationProtocol fragmentationProtocol = SimplePacketFragmenter.factory();
71 | private PacketFragmenter currentResponsePacket;
72 | private PacketDefragmenter currentPacketBuilder;
73 | private int mtu = 512;
74 |
75 | private AtomicLong requestId = new AtomicLong(0);
76 |
77 | public BlePeripheralService() {}
78 |
79 | @Override
80 | public IBinder onBind(Intent intent) {
81 | return binder;
82 | }
83 |
84 | @Override
85 | public boolean onUnbind(Intent intent) {
86 | return super.onUnbind(intent);
87 | }
88 |
89 | @Override
90 | public void onCreate() {
91 | close();
92 |
93 | btManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
94 | btAdapter = btManager.getAdapter();
95 | startServer();
96 | startAdvertisement();
97 |
98 | IntentFilter filter = new IntentFilter(ACTION);
99 | LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, filter);
100 | }
101 |
102 | public void onDestroy() {
103 | close();
104 | }
105 |
106 | public void startAdvertisement() {
107 | log("Starting to advertise device");
108 |
109 | // TODO: fails with NoSuchMethod on devices with API <= 19
110 | if (!getPackageManager().hasSystemFeature("android.hardware.bluetooth_le")) {
111 | log("BLE is not supported on this device");
112 | return;
113 | }
114 |
115 | if (!btAdapter.isMultipleAdvertisementSupported()) {
116 | log("No Advertising Support");
117 | return;
118 | }
119 |
120 | advertSettings = new AdvertiseSettings.Builder()
121 | .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
122 | .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
123 | .setConnectable(true)
124 | .setTimeout(0)
125 | .build();
126 |
127 | ParcelUuid pUuid = new ParcelUuid(BleCard.APDU_SERVICE_UUID);
128 |
129 | AdvertiseData data = new AdvertiseData.Builder()
130 | .setIncludeDeviceName(false)
131 | .addServiceUuid(pUuid)
132 | .build();
133 |
134 | final BluetoothLeAdvertiser advertiser = btAdapter.getBluetoothLeAdvertiser();
135 |
136 | if (advertiser == null) {
137 | log("Advertising is not supported on this device");
138 | Toast.makeText(this, "Advertising is not supported", Toast.LENGTH_LONG).show();
139 | return ;
140 | }
141 |
142 | if (advertCallback == null) {
143 | advertCallback = new AdvertiseCallback() {
144 | @Override
145 | public void onStartSuccess(AdvertiseSettings settingsInEffect) {
146 | super.onStartSuccess(settingsInEffect);
147 | log("Advertising started: " + settingsInEffect + ", device: " + btAdapter.getName() + ", addr: " + btAdapter.getAddress());
148 | }
149 |
150 | @Override
151 | public void onStartFailure(int errorCode) {
152 | super.onStartFailure(errorCode);
153 |
154 | String errCause = "unknown";
155 |
156 | switch(errorCode) {
157 | case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED:
158 | errCause = "Already started";
159 | break;
160 | case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE:
161 | errCause = "Data too large";
162 | break;
163 | case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
164 | errCause = "Feature unsupported";
165 | break;
166 | case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR:
167 | errCause = "Internal error";
168 | break;
169 | case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
170 | errCause = "Too many advertisers";
171 | break;
172 |
173 | }
174 |
175 | log("Advertising onStartFailure: " + errCause + "(" + errorCode + ")");
176 |
177 | close();
178 | }
179 | };
180 | }
181 |
182 |
183 | advertiser.startAdvertising(advertSettings, data, advertCallback);
184 | }
185 |
186 | public void startServer() {
187 | gattServerCallback = new BluetoothGattServerCallback() {
188 | @Override
189 | public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
190 | log("onConnectionStateChange: " + BleUtils.getStateDescription(newState));
191 |
192 | if (newState == BluetoothProfile.STATE_DISCONNECTED) {
193 | // Can cause calling finishConversation() twice on Conversation finished command and on connection close.
194 | // But as we only cleaning up the list – it won't harm, but with it in case of an error – we still sending cleanup commands.
195 | finishConversation();
196 | }
197 | }
198 |
199 | @Override
200 | public void onServiceAdded(int status, BluetoothGattService service) {
201 | log("serviceAdded: " + service.getUuid() + ", adding characteristics");
202 | }
203 |
204 | @Override
205 | public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
206 | super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
207 |
208 | if (characteristic.getUuid().equals(BleCard.APDU_MAX_MEMORY_FOR_APDU_PROCESSING)) {
209 | log("Returning max memory for APDU processing value. Characteristic: " + characteristic.getUuid());
210 | byte[] buf = new byte[4];
211 | BleUtils.packInt4(MAX_MEMORY, buf, 0);
212 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, buf);
213 | return ;
214 | }
215 |
216 |
217 | if (!characteristic.getUuid().equals(BleCard.APDU_READ_CHARACTERISTIC_UUID)) {
218 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
219 | log("Unsupported characteristics read: " + characteristic.getUuid());
220 | return ;
221 | }
222 |
223 | if (currentResponsePacket == null) {
224 | log("No answer ready yet");
225 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
226 | return;
227 | }
228 |
229 | if (currentResponsePacket.hasMoreData()) {
230 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, currentResponsePacket.nextFragment());
231 | } else {
232 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, new byte[]{0});
233 | }
234 |
235 |
236 | if (!currentResponsePacket.hasMoreData()) {
237 | currentResponsePacket = null;
238 | }
239 | }
240 |
241 | @Override
242 | public void onCharacteristicWriteRequest(final BluetoothDevice device, final int requestId,
243 | BluetoothGattCharacteristic characteristic,
244 | boolean preparedWrite, boolean responseNeeded,
245 | final int offset,
246 | byte[] value) {
247 | log("onCharacteristicWriteRequest(" + requestId + "): " + characteristic.getUuid() + ", value: " +
248 | BleUtils.byteArrayToString(value) +
249 | ", flags: prepared=" + preparedWrite + ", respNeeded=" + responseNeeded + ", offset: " + offset
250 | );
251 |
252 | if (responseNeeded) {
253 | BleUtils.retryCall(new Callable() {
254 | @Override
255 | public Boolean call() throws Exception {
256 | return gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[]{0});
257 | }
258 | });
259 | }
260 |
261 | if (offset != 0) {
262 | log("Offset is not zero: " + offset);
263 | return;
264 | }
265 |
266 | if(APDU_CONVERSATION_FINISHED_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
267 | finishConversation();
268 | return;
269 | }
270 |
271 |
272 | if (currentPacketBuilder == null) {
273 | log("Starting APDU request session");
274 | currentPacketBuilder = fragmentationProtocol.deframenter();
275 | }
276 |
277 | currentPacketBuilder.appendPacket(value);
278 |
279 | if (currentPacketBuilder.isCompleted()) {
280 | log("Packet received");
281 | sendBleAPDUToActivity(currentPacketBuilder.fullData());
282 | currentPacketBuilder = null;
283 | }
284 | }
285 |
286 | @Override
287 | public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
288 | super.onDescriptorReadRequest(device, requestId, offset, descriptor);
289 | log("onDescriptorReadRequest: " + descriptor.getUuid() + " char: " + descriptor.getCharacteristic().getUuid());
290 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, descriptor.getValue());
291 | }
292 |
293 | @Override
294 | public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor,
295 | boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
296 | super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
297 | log("onDescriptorWriteRequest: " + descriptor.getUuid() + " char: " + descriptor.getCharacteristic().getUuid());
298 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
299 | }
300 |
301 | @Override
302 | public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
303 | log("onExecuteWrite(" + requestId + "), execute: " + execute);
304 | gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
305 | }
306 |
307 | @Override
308 | public void onNotificationSent(BluetoothDevice device, int status) {
309 | log("onNotificationSent: " + device.getAddress() + ", status: " + status);
310 | }
311 |
312 | @Override
313 | public void onMtuChanged(BluetoothDevice device, int mtu) {
314 | log("onMtuChanged: " + mtu);
315 | }
316 | };
317 |
318 | log("Starting Gatt Server");
319 |
320 | if(gattServer == null) {
321 |
322 | gattServer = btManager.openGattServer(this, gattServerCallback);
323 |
324 | BluetoothGattService service = new BluetoothGattService(BleCard.APDU_SERVICE_UUID,
325 | BluetoothGattService.SERVICE_TYPE_PRIMARY);
326 |
327 | readNotifyCharacteristic =
328 | new BluetoothGattCharacteristic(BleCard.APDU_RESPONSE_READY_NOTIFY_CHARACTERISTIC_UUID,
329 | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
330 | BluetoothGattCharacteristic.PERMISSION_READ);
331 |
332 | // Notification descriptor is needed to be added to "readNotifyCharacteristic"
333 | BluetoothGattDescriptor gD = new BluetoothGattDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG),
334 | BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
335 | gD.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
336 | readNotifyCharacteristic.addDescriptor(gD);
337 |
338 | BluetoothGattCharacteristic readCharacteristic =
339 | new BluetoothGattCharacteristic(BleCard.APDU_READ_CHARACTERISTIC_UUID,
340 | BluetoothGattCharacteristic.PROPERTY_READ,
341 | BluetoothGattCharacteristic.PERMISSION_READ);
342 |
343 | BluetoothGattCharacteristic writeCharacteristic =
344 | new BluetoothGattCharacteristic(BleCard.APDU_WRITE_CHARACTERISTIC_UUID,
345 | BluetoothGattCharacteristic.PROPERTY_WRITE,
346 | BluetoothGattCharacteristic.PERMISSION_WRITE);
347 |
348 | BluetoothGattCharacteristic maxMemoryCharacteristic =
349 | new BluetoothGattCharacteristic(BleCard.APDU_MAX_MEMORY_FOR_APDU_PROCESSING,
350 | BluetoothGattCharacteristic.PROPERTY_READ,
351 | BluetoothGattCharacteristic.PERMISSION_READ);
352 |
353 | BluetoothGattCharacteristic finishConversationCharacteristic =
354 | new BluetoothGattCharacteristic(APDU_CONVERSATION_FINISHED_CHARACTERISTIC_UUID,
355 | BluetoothGattCharacteristic.PROPERTY_WRITE,
356 | BluetoothGattCharacteristic.PERMISSION_WRITE);
357 |
358 | service.addCharacteristic(readNotifyCharacteristic);
359 | service.addCharacteristic(maxMemoryCharacteristic);
360 | service.addCharacteristic(readCharacteristic);
361 | service.addCharacteristic(writeCharacteristic);
362 | service.addCharacteristic(finishConversationCharacteristic);
363 |
364 | boolean result = gattServer.addService(service);
365 | log("Added custom service: " + result);
366 |
367 | for(BluetoothGattService s : gattServer.getServices()) {
368 | log("Registered services: " + s.getUuid());
369 | }
370 | }
371 | }
372 |
373 | private void finishConversation() {
374 | Intent intent = new Intent(BlePeripheralService.CONVERSATION_FINISHED);
375 | LocalBroadcastManager.getInstance(BlePeripheralService.this).sendBroadcast(intent);
376 | }
377 |
378 | private void log(String s) {
379 | Log.i("BleService", s);
380 | Intent intent = new Intent(BlePeripheralService.LOG).putExtra("data", s);
381 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
382 |
383 | messagesList.add(s);
384 |
385 | if (messagesList.size() > MAX_LOG_BUFFER) {
386 | messagesList.removeFirst();
387 | }
388 | }
389 |
390 | private void sendBleAPDUToActivity(byte[] data) {
391 | Intent intent = new Intent(BlePeripheralService.BLE_APDU)
392 | .putExtra("apdu", data)
393 | .putExtra("id", requestId.get());
394 |
395 | log("APDU request session ended. Sending to a card. RequestId: " + requestId.get());
396 |
397 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
398 | }
399 |
400 | private void passCardResponse(byte[] response) {
401 | currentResponsePacket = fragmentationProtocol.fragmenter(mtu, response);
402 | notifyAllDevices("OK");
403 | log("Current Request Id: " + requestId.incrementAndGet());
404 | }
405 |
406 | private void notifyAllDevices(String notification) {
407 | readNotifyCharacteristic.setValue(notification);
408 |
409 | log("Notifying about the result");
410 |
411 | for (BluetoothDevice device: btManager.getConnectedDevices(GATT_SERVER)) {
412 | final BluetoothDevice localDevice = device;
413 | log("Notifying device: " + localDevice.getAddress());
414 |
415 | BleUtils.retryCall(new Callable() {
416 | @Override
417 | public Boolean call() throws Exception {
418 | return gattServer.notifyCharacteristicChanged(localDevice, readNotifyCharacteristic, false);
419 | }
420 | });
421 |
422 | break;
423 | }
424 | }
425 |
426 | public void close() {
427 | LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
428 |
429 | if (advertCallback != null) {
430 | btAdapter.getBluetoothLeAdvertiser().stopAdvertising(advertCallback);
431 | log("Advertising stopped");
432 | advertCallback = null;
433 | }
434 |
435 | if (gattServer != null) {
436 | gattServer.close();
437 | }
438 | }
439 |
440 | private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
441 | @Override
442 | public void onReceive(Context context, Intent intent) {
443 | switch(intent.getExtras().getString("cmd")) {
444 | case CMD_LOGS:
445 | StringBuilder sb = new StringBuilder();
446 |
447 | for (String record: messagesList) {
448 | sb.append(record).append("\n");
449 | }
450 |
451 | Intent localIntent = new Intent(BlePeripheralService.LOG).putExtra("data", sb.toString());
452 |
453 | LocalBroadcastManager.getInstance(BlePeripheralService.this)
454 | .sendBroadcast(localIntent);
455 |
456 | break;
457 | case CMD_SE_RESPONSE:
458 | byte[] response = intent.getByteArrayExtra("apdu-response");
459 | long requestId = intent.getLongExtra("id", -1);
460 | final long currentId = BlePeripheralService.this.requestId.get();
461 |
462 | if (currentId == requestId) {
463 | log("Card responded (" + requestId + ") " + byteArrayToString(response));
464 | passCardResponse(response);
465 | } else {
466 | log("Received request: " + requestId + ", but current id is: " + requestId);
467 | }
468 |
469 | break;
470 | case CMD_STOP:
471 | log("Stop command received");
472 | stopSelf();
473 | break;
474 |
475 | case NFC_RESPONSE:
476 | log("responding with... ");
477 | break;
478 | }
479 | }
480 | };
481 |
482 | public class LocalBinder extends Binder {}
483 | }
484 |
--------------------------------------------------------------------------------