/* * 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.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); // 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); } }