402 lines
14 KiB
Java
402 lines
14 KiB
Java
/*
|
|
* Copyright 2021 The Lucia Developers
|
|
*
|
|
* Licensed under the EUPL, Version 1.2 only (the "Licence");
|
|
* You may not use this work except in compliance with the Licence.
|
|
* You may obtain a copy of the Licence at:
|
|
*
|
|
* https://joinup.ec.europa.eu/software/page/eupl
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the Licence is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the Licence for the specific language governing permissions and
|
|
* limitations under the Licence.
|
|
*/
|
|
|
|
package digital.selfdefense.lucia;
|
|
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Point;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.util.Log;
|
|
import android.view.Display;
|
|
import android.view.WindowManager;
|
|
import android.widget.ImageView;
|
|
|
|
import androidx.appcompat.app.ActionBar;
|
|
import androidx.appcompat.app.AppCompatActivity;
|
|
|
|
import com.google.zxing.WriterException;
|
|
|
|
import java.security.InvalidKeyException;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.Arrays;
|
|
import java.util.UUID;
|
|
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
import androidmads.library.qrgenearator.QRGContents;
|
|
import androidmads.library.qrgenearator.QRGEncoder;
|
|
|
|
public class MainActivity extends AppCompatActivity {
|
|
|
|
Handler handler = new Handler();
|
|
Runnable runnable;
|
|
final int delay = 5000; // 5000 -> every 5 seconds
|
|
long lastTimestamp = 0;
|
|
|
|
/**
|
|
* Random UUID that is only around as long as the app runs.
|
|
* It is used to generate pseudo-random data.
|
|
*/
|
|
UUID randomTemporaryUuid = UUID.randomUUID();
|
|
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
setContentView(R.layout.activity_main);
|
|
|
|
// Change title
|
|
ActionBar actionBar = getSupportActionBar();
|
|
if(actionBar != null) {
|
|
actionBar.setTitle(R.string.banner_checkin);
|
|
}
|
|
|
|
// create new code on startup
|
|
this.createCode();
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
MainActivity self = this;
|
|
handler.postDelayed(runnable = new Runnable() {
|
|
public void run() {
|
|
handler.postDelayed(runnable, delay);
|
|
self.createCode();
|
|
}
|
|
}, delay);
|
|
super.onResume();
|
|
}
|
|
@Override
|
|
protected void onPause() {
|
|
super.onPause();
|
|
handler.removeCallbacks(runnable); //stop handler when activity not visible super.onPause();
|
|
}
|
|
|
|
/**
|
|
* Creates a new QR code and shows it. Only one code will be generated for
|
|
* every minute. If this method is run multiple times within the same minute
|
|
* only the first run will create a new code. Subsequent runs will silently
|
|
* return without action to save some energy.
|
|
*/
|
|
protected void createCode() {
|
|
|
|
/*
|
|
The code contains the following data:
|
|
|
|
1) version: 1 Byte, always 3
|
|
|
|
2) device type: 1 Byte, always 1 (for Android)
|
|
|
|
3) key id: 1 Byte, random value
|
|
|
|
4) unix timestamp: 4 Bytes, rounded down to latest full minute, little
|
|
endian byte order
|
|
|
|
5) random payload: 89 Bytes, random. In the "real" app, this would
|
|
contain encrypted data and a verification tag that isn't decrypted/
|
|
checked upon check-in. Before a decryption attempt this looks like
|
|
random data, so this app will fill in random data to make it look
|
|
authentic.
|
|
|
|
6) checksum: 4 Bytes, the first 4 Bytes of the SHA256 digest of fields
|
|
1 to 5 concatenated.
|
|
|
|
During the check-in procedure only fields 1, 2, 4 and 6 are checked. The
|
|
others cannot be verified at the time of check-in and are stored for
|
|
later use. Since this app only aims to let people check-in anonymously,
|
|
this is acceptable.
|
|
*/
|
|
|
|
/**
|
|
* Field 1: Code version - always 3
|
|
*/
|
|
byte[] version = new byte[] {3};
|
|
|
|
/**
|
|
* Field 2: Device type - always 1 on Android devices
|
|
*
|
|
* 0 means iOS device
|
|
* 1 means Android device
|
|
* 2 means static QR code used on key tags
|
|
*/
|
|
byte[] deviceType = new byte[] {1};
|
|
|
|
// temporary variables in preparation for field 4
|
|
long unixTimestamp = System.currentTimeMillis() / 1000L;
|
|
long unixTimestampRounded = unixTimestamp / 60;
|
|
unixTimestampRounded *= 60;
|
|
|
|
/**
|
|
* Field 4: unix timestamp - rounded to current full minute, little
|
|
* endian byte order
|
|
*/
|
|
byte[] unixTimestampRoundedLE = Arrays.copyOfRange(longToBytesLE(unixTimestampRounded), 0, 4);
|
|
|
|
/**
|
|
* Field 3: key id - 1 random Byte
|
|
*/
|
|
byte[] keyId = generateRandomBytes(1, "lucia:code:part:keyId:uuid-" + randomTemporaryUuid.toString() + ":ts-" + unixTimestampRounded);
|
|
|
|
/**
|
|
* Field 5: random payload - 89 random Bytes
|
|
*/
|
|
byte[] randomPayload = generateRandomBytes(89, "lucia:code:part:randomPayload:uuid-" + randomTemporaryUuid.toString() + ":ts-" + unixTimestampRounded);
|
|
|
|
// abort early if timestamp hasn't changed because generating the code
|
|
// multiple times within the same minute will yield the same code
|
|
if(this.lastTimestamp == unixTimestampRounded) {
|
|
return;
|
|
}
|
|
else{
|
|
this.lastTimestamp = unixTimestampRounded;
|
|
}
|
|
|
|
// concatenate fields 1 to 5 into a byte array in preparation for the
|
|
// checksum calculation
|
|
byte[] rawCodeWithoutChecksum = new byte[version.length
|
|
+ deviceType.length
|
|
+ keyId.length
|
|
+ unixTimestampRoundedLE.length
|
|
+ randomPayload.length];
|
|
System.arraycopy(
|
|
version,
|
|
0,
|
|
rawCodeWithoutChecksum,
|
|
0,
|
|
version.length);
|
|
System.arraycopy(
|
|
deviceType,
|
|
0,
|
|
rawCodeWithoutChecksum,
|
|
version.length,
|
|
deviceType.length);
|
|
System.arraycopy(
|
|
keyId,
|
|
0,
|
|
rawCodeWithoutChecksum,
|
|
version.length + deviceType.length,
|
|
keyId.length);
|
|
System.arraycopy(
|
|
unixTimestampRoundedLE,
|
|
0,
|
|
rawCodeWithoutChecksum,
|
|
version.length + deviceType.length + keyId.length,
|
|
unixTimestampRoundedLE.length);
|
|
System.arraycopy(
|
|
randomPayload,
|
|
0,
|
|
rawCodeWithoutChecksum,
|
|
version.length + deviceType.length + keyId.length + unixTimestampRoundedLE.length,
|
|
randomPayload.length);
|
|
|
|
boolean success = true;
|
|
|
|
/**
|
|
* Field 6: checksum - first 4 Bytes of the SHA256 digest of the other
|
|
* fields
|
|
*/
|
|
byte[] checksum = new byte[4];
|
|
|
|
try {
|
|
// Calculate the checksum
|
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
md.update(rawCodeWithoutChecksum);
|
|
byte[] digest = md.digest();
|
|
|
|
// copy the first 4 Bytes of the checksum into the appropriate var
|
|
System.arraycopy(
|
|
digest,
|
|
0,
|
|
checksum,
|
|
0,
|
|
checksum.length);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
// this catches an error that only occurs of the "SHA-256" hashing
|
|
// algorithm cannot be found. This should never happen but we will
|
|
// catch the error anyways for good measure.
|
|
e.printStackTrace();
|
|
success = false;
|
|
}
|
|
|
|
if(success) {
|
|
// append the checksum to the previously concatenated code parts
|
|
byte[] rawCode = new byte[rawCodeWithoutChecksum.length + checksum.length];
|
|
System.arraycopy(
|
|
rawCodeWithoutChecksum,
|
|
0,
|
|
rawCode,
|
|
0,
|
|
rawCodeWithoutChecksum.length);
|
|
System.arraycopy(
|
|
checksum,
|
|
0,
|
|
rawCode,
|
|
rawCodeWithoutChecksum.length,
|
|
checksum.length);
|
|
|
|
// encode the raw bytes using Z85 to make it "printable"
|
|
String code = Z85.encode(rawCode);
|
|
|
|
// generate the QR code and show it
|
|
showQrCode(code);
|
|
|
|
// log the code components and final result to logcat
|
|
Log.i(
|
|
"Lucia Code",
|
|
"{\n"
|
|
+ "version: " + bytesToHex(version) + "\n"
|
|
+ "deviceType: " + bytesToHex(deviceType) + "\n"
|
|
+ "keyId: " + bytesToHex(keyId) + "\n"
|
|
+ "timestamp: " + bytesToHex(unixTimestampRoundedLE) + "\n"
|
|
+ "randomPayload: " + bytesToHex(randomPayload) + "\n"
|
|
+ "checksum: " + bytesToHex(checksum) + "\n"
|
|
+ "code: " + code + "\n"
|
|
+ "}"
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns a byte array containing pseudo random bytes of a given length.
|
|
* This function is good enough for this app but DO NOT USE THIS FUNCTION TO
|
|
* CREATE RANDOM NUMBERS FOR CRYPTOGRAPHY!
|
|
* @param len the desired length of the byte array
|
|
* @param pers a String on which the random data will be generated out of.
|
|
* If you call this function twice with the same pers value,
|
|
* the exact same sequence of pseudo random bytes will be
|
|
* generated. This is why the calling function above includes
|
|
* a random UUID and the current timestamp into the pers value.
|
|
* @return a byte array containing len pseudo random bytes
|
|
*/
|
|
protected byte[] generateRandomBytes(int len, String pers) {
|
|
final String hashingAlgo = "HmacSHA256";
|
|
final int hashingBytes = 32;
|
|
try {
|
|
int count = 0;
|
|
byte[] init = hmac(hashingAlgo, pers.getBytes(), intToBytesBE(count));
|
|
byte[] result = new byte[]{};
|
|
for (count = 1; count <= (len / 4) + 1; count++) {
|
|
byte[] newResult = hmac(hashingAlgo, init, intToBytesBE(count));
|
|
byte[] oldResult = result;
|
|
result = new byte[oldResult.length + newResult.length];
|
|
System.arraycopy(oldResult, 0, result, 0, oldResult.length);
|
|
System.arraycopy(newResult, 0, result, oldResult.length, newResult.length);
|
|
}
|
|
return Arrays.copyOfRange(result, 0, len);
|
|
}
|
|
catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
|
// in case of emergency return all zeros
|
|
byte[] result = new byte[len];
|
|
for(int i = 0; i < len; i++){
|
|
result[i] = 0;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper function for generating HMACs
|
|
* @param algorithm
|
|
* @param key
|
|
* @param message
|
|
* @return
|
|
* @throws NoSuchAlgorithmException
|
|
* @throws InvalidKeyException
|
|
*/
|
|
public static byte[] hmac(String algorithm, byte[] key, byte[] message) throws NoSuchAlgorithmException, InvalidKeyException {
|
|
Mac mac = Mac.getInstance(algorithm);
|
|
mac.init(new SecretKeySpec(key, algorithm));
|
|
return mac.doFinal(message);
|
|
}
|
|
|
|
/**
|
|
* Converts an int to a byte array containing 4 Bytes in big endian order
|
|
* @param data
|
|
* @return
|
|
*/
|
|
private static byte[] intToBytesBE(final int data) {
|
|
return new byte[] {
|
|
(byte)((data >> 24) & 0xff),
|
|
(byte)((data >> 16) & 0xff),
|
|
(byte)((data >> 8) & 0xff),
|
|
(byte)((data >> 0) & 0xff),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts a long to a byte array containing 8 Bytes in little endian order
|
|
* @param data
|
|
* @return
|
|
*/
|
|
private static byte[] longToBytesLE(final long data) {
|
|
return new byte[] {
|
|
(byte)((data >> 0) & 0xff),
|
|
(byte)((data >> 8) & 0xff),
|
|
(byte)((data >> 16) & 0xff),
|
|
(byte)((data >> 24) & 0xff),
|
|
(byte)((data >> 32) & 0xff),
|
|
(byte)((data >> 40) & 0xff),
|
|
(byte)((data >> 48) & 0xff),
|
|
(byte)((data >> 56) & 0xff),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a QR code of a given String and shows it in the GUI
|
|
* @param text
|
|
*/
|
|
protected void showQrCode(String text) {
|
|
WindowManager manager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
|
Display display = manager.getDefaultDisplay();
|
|
Point point = new Point();
|
|
display.getSize(point);
|
|
int width = point.x;
|
|
int height = point.y;
|
|
int smallerDimension = width < height ? width : height;
|
|
smallerDimension = smallerDimension * 3 / 4;
|
|
QRGEncoder qrgEncoder = new QRGEncoder(text, null, QRGContents.Type.TEXT, smallerDimension);
|
|
ImageView qrImage = (ImageView) findViewById(R.id.imageView2);
|
|
try {
|
|
Bitmap bitmap = qrgEncoder.encodeAsBitmap();
|
|
qrImage.setImageBitmap(bitmap);
|
|
} catch (WriterException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The hexadecimal alphabet
|
|
*/
|
|
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
|
|
|
/**
|
|
* Converts a byte array to a hexadecimal String
|
|
* @param bytes
|
|
* @return
|
|
*/
|
|
public static String bytesToHex(byte[] bytes) {
|
|
char[] hexChars = new char[bytes.length * 2];
|
|
for (int j = 0; j < bytes.length; j++) {
|
|
int v = bytes[j] & 0xFF;
|
|
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
|
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
|
}
|
|
return new String(hexChars);
|
|
}
|
|
} |