Skip to content

Commit

Permalink
Merge pull request #21 from privacybydesign/security-fixes
Browse files Browse the repository at this point in the history
Security fixes, dependency updates, gradle upgrade, TomEE 9 support
  • Loading branch information
bobhageman authored Sep 5, 2023
2 parents 3cd9fac + c2992d7 commit f42c57b
Show file tree
Hide file tree
Showing 21 changed files with 586 additions and 76 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ src/main/resources/config.json
id_rsa*
webapp/node_modules
webapp/build
webapp/config.js
artifacts
.idea/
bin/
bin/
.DS_Store
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"java.configuration.updateBuildConfiguration": "automatic",
"java.compile.nullAnalysis.mode": "automatic"
}
26 changes: 26 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

FROM yarnpkg/node-yarn:latest as webappbuild

ARG LANGUAGE=en

# Build the webapp
COPY ./webapp/ /webapp/
WORKDIR /webapp
RUN yarn install && ./build.sh ${LANGUAGE}

FROM gradle:7.6-jdk11 as javabuild

# Build the java app
COPY ./ /app/
WORKDIR /app
RUN gradle build

FROM tomee:9.1-jre11

# Copy the webapp to the webapps directory
COPY --from=webappbuild /webapp/build/ /usr/local/tomee/webapps/ROOT/

# Copy the war file to the webapps directory
COPY --from=javabuild /app/build/libs/irma_sms_issuer-1.0.war /usr/local/tomee/webapps/

EXPOSE 8080
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# irma_sms_issuer
The IRMA SMS issuer takes care of issuing a mobile phone number to [Yivi app](https://github.com/privacybydesign/irmamobile) users. It consists of a Java backend API, which connects to an [irma server (issuer)](https://github.com/privacybydesign/irmago) and an SMS gateway service, and a frontend web app.

# Running (development)
The easiest way to run the irma_sms_issuer for development purposes is by having a phone with the Yivi app installed and Docker.

### Setup listening for SMS messages:

To be able to get the verification code execute [socat](http://www.dest-unreach.org/socat) in a separate terminal to intercept relevant local traffic:
```bash
$ socat -v TCP-LISTEN:8766,crlf,reuseaddr,fork SYSTEM:"echo HTTP/1.1 200"
```

If you have an Android device you can also install [StartHere SMS Gateway App](https://m.apkpure.com/starthere-sms-gateway-app/com.bogdan.sms). This will give you a more visual experience. Make sure your development machine and phone are on the same network. And then, when the app is started, it runs a local server imitating an SMS sending gateway. SMS messages, in the form of POST requests coming from irma_sms_issuer, are sent to this messaging service and will be displayed inside the app.

### Configuration
Various configuration files, keys and settings need to be in place to be able to build and run the apps.

1. To generate the required keys, run:
```bash
$ utils/keygen.sh ./src/main/resources/sk ./src/main/resources/pk
```

2. Create the Java app configuration:
Copy the file `src/main/resources/config.sample.json` to `src/main/resources/config.json` and set the `sms_sender_address` to match the IP address of your localhost or the Address displayed in the StartHere SMS Gateway app. For example:

```json
{
"sms_sender_address": "http://192.168.1.100:8766",
}
```

### Run
Use docker-compose up combined with your localhost IP address as environment variable to spin up the containers:
```bash
$ IP=192.168.1.105 docker-compose up
```
Note: do not use `127.0.0.1` or `0.0.0.0` as IP addresses as this will result in the app not being able to find the issuer.

By default, docker-compose caches docker images, so on a second run the previous built images will be used. A fresh build can be enforced using the --build flag.
```bash
$ IP=192.168.1.105 docker-compose up --build
```

Navigate to http://localhost:8080 in your browser and follow the instructions to test the complete flow.

## Manually
The Java api and JavaScript frontend can be built and run manually. To do so:

### Build

1. Generate JWT keys for the issuer
```bash
$ utils/keygen.sh ./src/main/resources/sk ./src/main/resources/pk
```

2. Copy the file `src/main/resources/config.sample.json` to `src/main/resources/config.json` and modify it.

3. Build the webapp:
```bash
$ cd webapp
$ yarn install
$ yarn build en
```
The last command builds the English version of the webapp. To build another language, for example Dutch, run `yarn build nl` instead.

4. Copy the file `webapp/config.example.js` to `webapp/build/assets/config.js` and modify it

5. Run the following command in the root directory of this project:
```bash
$ gradle appRun
```

To open the webapp navigate to http://localhost:8080/irma_sms_issuer. The API is accessible via http://localhost:8080/irma_sms_issuer/api.

### Test
You can run the tests, defined in `src/test/java/foundation/privacybydesign/sms/ratelimit`, using the following command:
```bash
$ gradle test
```
41 changes: 24 additions & 17 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@
group = 'foundation.privacybydesign.sms'
version = '1.0'
group 'foundation.privacybydesign.sms'
version '1.0'

apply plugin: 'war'
apply plugin: 'org.akhikhl.gretty'
apply plugin: 'org.gretty'

sourceCompatibility = 1.7
sourceCompatibility = 11

buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}

}
dependencies {
classpath "gradle.plugin.org.akhikhl.gretty:gretty:1.4.2"
classpath "org.gretty:gretty:4.0.3"
}
}

repositories {
mavenLocal()
maven {
url "https://credentials.github.io/repos/maven2/"
}
mavenCentral()
}

dependencies {
compile 'org.glassfish.jersey.core:jersey-server:2.25'
compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.25'
compile 'org.slf4j:slf4j-simple:1.7.25'
compile 'com.jcraft:jsch:0.1.53'

compile 'org.irmacard.api:irma_api_common:1.2.1'
compile 'foundation.privacybydesign.common:irma_server_common:0.3.2'

compile 'com.googlecode.libphonenumber:libphonenumber:8.9.10'

testCompile group: 'junit', name: 'junit', version: '4.12'
implementation 'org.glassfish.jersey.core:jersey-server:3.0.0'
implementation 'org.glassfish.jersey.containers:jersey-container-servlet:3.0.0'
implementation 'org.glassfish.jersey.inject:jersey-hk2:3.0.0'
implementation 'org.slf4j:slf4j-simple:1.7.25'
implementation 'com.jcraft:jsch:0.1.53'
implementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.9.10'

implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.apache.commons:commons-lang3:3.7'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
implementation 'org.bouncycastle:bcprov-jdk15on:1.67'

implementation 'org.irmacard.api:irma_api_common:2.0.0'

testImplementation group: 'junit', name: 'junit', version: '4.12'
}

configurations.all {
Expand All @@ -48,4 +54,5 @@ configurations.all {

gretty {
contextConfigFile = file('src/test/resources/jetty-env.xml')
extraResourceBase 'webapp/build'
}
40 changes: 40 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version: "3.8"
name: irma_sms_issuer

services:
# Irma issuer service
irmaserver:
image: ghcr.io/privacybydesign/irma:v0.13.1
working_dir: /irmago
ports:
- 8088:8088
expose:
- 8088
entrypoint:
- "irma"
- "server"
- "--no-auth=false"
- "--requestors={\"irma_sms_issuer\":{\"auth_method\":\"publickey\",\"key_file\": \"/config/pk.pem\"} }"
- "--port=8088"
- "--jwt-privkey-file=/config/sk.pem"
- "--url=http://${IP}:8088"
volumes:
- ./src/main/resources/:/config/

# Service that runs the SMS issuer webapp and api
irma_sms_issuer:
platform: linux/x86_64
build:
context: .
dockerfile: Dockerfile
environment:
- IRMA_CONF=/config/
volumes:
# Make keys and config.json available to Java app
- ./src/main/resources/:/config/
# Make config.js available to webapp
- ./webapp/config.example.js:/usr/local/tomee/webapps/ROOT/assets/config.js:ro"
ports:
- 8080:8080
expose:
- 8080
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package foundation.privacybydesign.sms;

import foundation.privacybydesign.common.BaseConfiguration;
import foundation.privacybydesign.sms.common.BaseConfiguration;
import io.jsonwebtoken.SignatureAlgorithm;
import org.irmacard.api.common.util.GsonUtil;

Expand All @@ -13,7 +13,7 @@
* Configuration manager. The config itself is stored in config.json, which
* is probably located in build/resources/main.
*/
public class SMSConfiguration extends BaseConfiguration {
public class SMSConfiguration extends BaseConfiguration<SMSConfiguration> {
static SMSConfiguration instance;
static final String CONFIG_FILENAME = "config.json";
static {
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/foundation/privacybydesign/sms/SMSRestApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.security.KeyManagementException;
import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.ApplicationPath;
import jakarta.ws.rs.ApplicationPath;

/**
* Boilerplate: set up the REST API in SMSRestApi.
Expand Down
19 changes: 17 additions & 2 deletions src/main/java/foundation/privacybydesign/sms/TokenManager.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package foundation.privacybydesign.sms;

import foundation.privacybydesign.common.CryptoUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -60,12 +59,13 @@ public boolean verify(String phone, String token) {
logger.error("Phone number not found");
return false;
}

if (tr.isExpired()) {
// Expired, but not yet cleaned out by periodicCleanup()
logger.error("Token expired");
return false;
}
if (!CryptoUtil.isEqualsConstantTime(tr.token.toCharArray(), token.toCharArray())) {
if (!isEqualsConstantTime(tr.token.toCharArray(), token.toCharArray())) {
tr.tries++;
logger.error("Token is wrong");
return false;
Expand All @@ -80,6 +80,21 @@ public boolean verify(String phone, String token) {
return true;
}

/**
* Compare two byte arrays in constant time.
*/
public static boolean isEqualsConstantTime(char[] a, char[] b) {
if (a.length != b.length) {
return false;
}

byte result = 0;
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}

void periodicCleanup() {
// Use enhanced for loop, because an iterator makes sure concurrency issues cannot occur.
for (Map.Entry<String, TokenRequest> entry : tokenMap.entrySet()) {
Expand Down
Loading

0 comments on commit f42c57b

Please sign in to comment.