diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Action.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Action.java index c8de9d4f1d..f40335e3bb 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Action.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Action.java @@ -1,7 +1,7 @@ package com.github.dedis.popstellar.model.network.method.message.data; -import java.util.Objects; import java.util.*; +import java.util.Objects; /** Enumerates all possible messages actions */ public enum Action { @@ -30,7 +30,8 @@ public enum Action { DELETE("delete"), NOTIFY_DELETE("notify_delete"), KEY("key"), - POST_TRANSACTION("post_transaction"); + POST_TRANSACTION("post_transaction"), + AUTH("auth"); private static final List ALL = Collections.unmodifiableList(Arrays.asList(values())); private final String action; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Objects.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Objects.java index e5dc2a8a13..9064af18bc 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Objects.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/Objects.java @@ -12,7 +12,8 @@ public enum Objects { CONSENSUS("consensus"), CHIRP("chirp"), REACTION("reaction"), - COIN("coin"); + COIN("coin"), + POPCHA("popcha"); private static final List ALL = Collections.unmodifiableList(Arrays.asList(values())); private final String object; @@ -62,6 +63,7 @@ public boolean hasToBePersisted() { return true; // TODO: add persistence for consensus when it'll have its repo case "consensus": + case "popcha": default: return false; } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/popcha/PoPCHAAuthentication.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/popcha/PoPCHAAuthentication.java new file mode 100644 index 0000000000..28a05c28f9 --- /dev/null +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/network/method/message/data/popcha/PoPCHAAuthentication.java @@ -0,0 +1,103 @@ +package com.github.dedis.popstellar.model.network.method.message.data.popcha; + +import androidx.annotation.Nullable; +import com.github.dedis.popstellar.model.Immutable; +import com.github.dedis.popstellar.model.network.method.message.data.*; +import com.google.gson.annotations.SerializedName; + +/** Data sent to authenticate to a PoPCHA server */ +@Immutable +public class PoPCHAAuthentication extends Data { + + @SerializedName("client_id") + private final String clientId; + + private final String nonce; + private final String identifier; + + @SerializedName("identifier_proof") + private final String identifierProof; + + @Nullable private final String state; + + @Nullable + @SerializedName("response_mode") + private final String responseMode; + + @SerializedName("popcha_address") + private final String popchaAddress; + + public PoPCHAAuthentication( + String clientId, + String nonce, + String identifier, + String identifierProof, + @Nullable String state, + @Nullable String responseMode, + String popchaAddress) { + this.clientId = clientId; + this.nonce = nonce; + this.identifier = identifier; + this.identifierProof = identifierProof; + this.state = state; + this.responseMode = responseMode; + this.popchaAddress = popchaAddress; + } + + @Override + public String getObject() { + return Objects.POPCHA.getObject(); + } + + @Override + public String getAction() { + return Action.AUTH.getAction(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PoPCHAAuthentication that = (PoPCHAAuthentication) o; + return clientId.equals(that.clientId) + && nonce.equals(that.nonce) + && identifier.equals(that.identifier) + && identifierProof.equals(that.identifierProof) + && java.util.Objects.equals(state, that.state) + && java.util.Objects.equals(responseMode, that.responseMode) + && popchaAddress.equals(that.popchaAddress); + } + + @Override + public int hashCode() { + return java.util.Objects.hash( + clientId, nonce, identifier, identifierProof, state, responseMode, popchaAddress); + } + + @Override + public String toString() { + return "PoPCHAAuthentication{" + + "clientId='" + + clientId + + '\'' + + ", nonce='" + + nonce + + '\'' + + ", identifier='" + + identifier + + '\'' + + ", identifierProof='" + + identifierProof + + '\'' + + ", state='" + + state + + '\'' + + ", responseMode='" + + responseMode + + '\'' + + ", popchaAddress='" + + popchaAddress + + '\'' + + '}'; + } +} diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/qrcode/PoPCHAQRCode.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/qrcode/PoPCHAQRCode.java new file mode 100644 index 0000000000..fbda9fb508 --- /dev/null +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/qrcode/PoPCHAQRCode.java @@ -0,0 +1,75 @@ +package com.github.dedis.popstellar.model.qrcode; + +import android.net.Uri; +import com.github.dedis.popstellar.model.Immutable; +import com.github.dedis.popstellar.utility.MessageValidator; + +@Immutable +public class PoPCHAQRCode { + + public static final String CLIENT_ID = "client_id"; + public static final String NONCE = "nonce"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String STATE = "state"; + public static final String RESPONSE_TYPE = "response_type"; + public static final String RESPONSE_MODE = "response_mode"; + public static final String LOGIN_HINT = "login_hint"; + + private final String clientId; + private final String nonce; + private final String state; + private final String responseMode; + private final String host; + + public PoPCHAQRCode(String data, String laoId) throws IllegalArgumentException { + MessageValidator.verify().isValidPoPCHAUrl(data, laoId); + + Uri uri = Uri.parse(data); + clientId = uri.getQueryParameter(CLIENT_ID); + nonce = uri.getQueryParameter(NONCE); + state = uri.getQueryParameter(STATE); + responseMode = uri.getQueryParameter(RESPONSE_MODE); + host = uri.getHost(); + } + + public String getClientId() { + return clientId; + } + + public String getNonce() { + return nonce; + } + + public String getState() { + return state; + } + + public String getResponseMode() { + return responseMode; + } + + public String getHost() { + return host; + } + + @Override + public String toString() { + return "PoPCHAQRCode{" + + "clientId='" + + clientId + + '\'' + + ", nonce='" + + nonce + + '\'' + + ", state='" + + state + + '\'' + + ", responseMode='" + + responseMode + + '\'' + + ", host='" + + host + + '\'' + + '}'; + } +} diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.java index 6377a0108f..f4291323c6 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.java @@ -2,10 +2,8 @@ import android.app.Application; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.lifecycle.*; - import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.model.objects.Wallet; import com.github.dedis.popstellar.model.objects.view.LaoView; @@ -22,18 +20,15 @@ import com.github.dedis.popstellar.utility.error.keys.SeedValidationException; import com.google.gson.Gson; import com.google.gson.JsonParseException; - +import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.BackpressureStrategy; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import java.security.GeneralSecurityException; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; - import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import io.reactivex.BackpressureStrategy; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; import timber.log.Timber; @HiltViewModel @@ -121,7 +116,7 @@ public void handleData(String data) { Timber.tag(TAG).e(e, "Invalid QRCode laoData"); Toast.makeText( getApplication().getApplicationContext(), - R.string.invalid_qrcode_data, + R.string.invalid_qrcode_lao_data, Toast.LENGTH_LONG) .show(); return; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.java index 56bc435d43..5cfa4d4d19 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.java @@ -66,6 +66,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { witnessingViewModel = obtainWitnessingViewModel(this, laoId); + obtainPoPCHAViewModel(this, laoId).disableConnectingFlag(); + // At creation of the lao activity the connections of the lao are restored from the persistent // storage, such that the client resubscribes to each previous subscribed channel laoViewModel.restoreConnections(); diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAHomeFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAHomeFragment.java index d09d39f295..79ff66c973 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAHomeFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAHomeFragment.java @@ -1,5 +1,7 @@ package com.github.dedis.popstellar.ui.lao.popcha; +import static com.github.dedis.popstellar.ui.lao.LaoActivity.setCurrentFragment; + import android.os.Bundle; import android.view.*; import androidx.annotation.NonNull; @@ -9,11 +11,13 @@ import com.github.dedis.popstellar.databinding.PopchaHomeFragmentBinding; import com.github.dedis.popstellar.ui.lao.LaoActivity; import com.github.dedis.popstellar.ui.lao.LaoViewModel; +import com.github.dedis.popstellar.ui.qrcode.QrScannerFragment; +import com.github.dedis.popstellar.ui.qrcode.ScanningAction; public class PoPCHAHomeFragment extends Fragment { - private PopchaHomeFragmentBinding binding; + private LaoViewModel laoViewModel; - private PoPCHAViewModel poPCHAViewModel; + private PoPCHAViewModel popCHAViewModel; public PoPCHAHomeFragment() { // Required public empty constructor @@ -30,14 +34,26 @@ public View onCreateView( @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { laoViewModel = LaoActivity.obtainViewModel(requireActivity()); - poPCHAViewModel = LaoActivity.obtainPoPCHAViewModel(requireActivity(), laoViewModel.getLaoId()); - binding = PopchaHomeFragmentBinding.inflate(inflater, container, false); + popCHAViewModel = LaoActivity.obtainPoPCHAViewModel(requireActivity(), laoViewModel.getLaoId()); + PopchaHomeFragmentBinding binding = + PopchaHomeFragmentBinding.inflate(inflater, container, false); binding.popchaHeader.setText( String.format( - getResources().getString(R.string.popcha_header), poPCHAViewModel.getLaoId())); + getResources().getString(R.string.popcha_header), popCHAViewModel.getLaoId())); + + binding.popchaScanner.setOnClickListener(v -> openScanner()); - binding.popchaScanner.setOnClickListener(v -> {}); + popCHAViewModel + .getTextDisplayed() + .observe( + getViewLifecycleOwner(), + stringSingleEvent -> { + String url = stringSingleEvent.getContentIfNotHandled(); + if (url != null) { + binding.popchaText.setText(url); + } + }); handleBackNav(); return binding.getRoot(); @@ -50,6 +66,14 @@ public void onResume() { laoViewModel.setIsTab(true); } + private void openScanner() { + laoViewModel.setIsTab(false); + setCurrentFragment( + getParentFragmentManager(), + R.id.fragment_qr_scanner, + () -> QrScannerFragment.newInstance(ScanningAction.ADD_POPCHA)); + } + public static void openFragment(FragmentManager manager) { LaoActivity.setCurrentFragment(manager, R.id.fragment_popcha_home, PoPCHAHomeFragment::new); } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAViewModel.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAViewModel.java index e1fe94e5df..41a6698b75 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAViewModel.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/popcha/PoPCHAViewModel.java @@ -1,22 +1,63 @@ package com.github.dedis.popstellar.ui.lao.popcha; import android.app.Application; +import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; +import androidx.lifecycle.*; +import com.github.dedis.popstellar.R; +import com.github.dedis.popstellar.SingleEvent; +import com.github.dedis.popstellar.model.network.method.message.data.popcha.PoPCHAAuthentication; +import com.github.dedis.popstellar.model.objects.Channel; +import com.github.dedis.popstellar.model.objects.security.*; +import com.github.dedis.popstellar.model.objects.view.LaoView; +import com.github.dedis.popstellar.model.qrcode.PoPCHAQRCode; +import com.github.dedis.popstellar.repository.LAORepository; +import com.github.dedis.popstellar.repository.remote.GlobalNetworkManager; import com.github.dedis.popstellar.ui.qrcode.QRCodeScanningViewModel; +import com.github.dedis.popstellar.utility.error.UnknownLaoException; +import com.github.dedis.popstellar.utility.error.keys.KeyException; +import com.github.dedis.popstellar.utility.security.KeyManager; import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.disposables.CompositeDisposable; +import java.security.GeneralSecurityException; +import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; +import timber.log.Timber; @HiltViewModel public class PoPCHAViewModel extends AndroidViewModel implements QRCodeScanningViewModel { + private static final String TAG = PoPCHAViewModel.class.getSimpleName(); + + private static final String AUTHENTICATION = "authentication"; + + private String laoId; + private final MutableLiveData> textDisplayed = new MutableLiveData<>(); + private final AtomicBoolean connecting = new AtomicBoolean(false); + private final CompositeDisposable disposables = new CompositeDisposable(); + + /* Dependencies to inject */ + private final LAORepository laoRepository; + private final GlobalNetworkManager networkManager; + private final KeyManager keyManager; + @Inject - public PoPCHAViewModel(@NonNull Application application) { + public PoPCHAViewModel( + @NonNull Application application, + LAORepository laoRepository, + GlobalNetworkManager networkManager, + KeyManager keyManager) { super(application); + this.laoRepository = laoRepository; + this.networkManager = networkManager; + this.keyManager = keyManager; } - private String laoId; + @Override + protected void onCleared() { + super.onCleared(); + disposables.dispose(); + } public void setLaoId(String laoId) { this.laoId = laoId; @@ -26,11 +67,87 @@ public String getLaoId() { return laoId; } - @Override - public void handleData(String data) {} + public MutableLiveData> getTextDisplayed() { + return textDisplayed; + } + + private void postTextDisplayed(String text) { + textDisplayed.postValue(new SingleEvent<>(text)); + } + + private LaoView getLao() throws UnknownLaoException { + return laoRepository.getLaoView(laoId); + } + + public void handleData(String data) { + // Don't process another data from the scanner if I'm already trying to connect + if (connecting.get()) { + return; + } + connecting.set(true); + PoPCHAQRCode popCHAQRCode; + try { + popCHAQRCode = new PoPCHAQRCode(data, laoId); + } catch (IllegalArgumentException e) { + Timber.tag(TAG).e(e, "Invalid QRCode PoPCHAData"); + Toast.makeText( + getApplication().getApplicationContext(), + R.string.invalid_qrcode_popcha_data, + Toast.LENGTH_LONG) + .show(); + connecting.set(false); + return; + } + PoPToken token; + try { + token = keyManager.getLongTermPoPToken(laoId, popCHAQRCode.getClientId()); + } catch (KeyException e) { + Timber.tag(TAG).e(e, "Impossible to generate the token"); + connecting.set(false); + return; + } + + postTextDisplayed(popCHAQRCode.toString()); + try { + sendAuthRequest(popCHAQRCode, token); + } catch (GeneralSecurityException | UnknownLaoException e) { + if (e instanceof GeneralSecurityException) { + Timber.tag(TAG).e(e, "Impossible to generate sign the token"); + } else { + Timber.tag(TAG).e(e, "Impossible to find lao"); + } + connecting.set(false); + } + } + + private void sendAuthRequest(PoPCHAQRCode popCHAQRCode, PoPToken token) + throws GeneralSecurityException, UnknownLaoException { + Base64URLData nonce = new Base64URLData(popCHAQRCode.getNonce()); + Signature signedToken = token.sign(nonce); + PoPCHAAuthentication authMessage = + new PoPCHAAuthentication( + popCHAQRCode.getClientId(), + nonce.toString(), + token.getPublicKey().getEncoded(), + signedToken.getEncoded(), + popCHAQRCode.getHost(), + popCHAQRCode.getState(), + popCHAQRCode.getResponseMode()); + Channel channel = getLao().getChannel().subChannel(AUTHENTICATION); + disposables.add( + networkManager + .getMessageSender() + .publish(keyManager.getMainKeyPair(), channel, authMessage) + .subscribe(() -> Timber.tag(TAG).d("sent the auth message for popcha"))); + } + + public void disableConnectingFlag() { + connecting.set(false); + } @Override public LiveData getNbScanned() { - return null; + // This is useless for the PoPCHA Scanner (we just scan once) + return new MutableLiveData<>(0); } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QrScannerFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QrScannerFragment.java index 33591ed865..eef4bac269 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QrScannerFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QrScannerFragment.java @@ -1,9 +1,12 @@ package com.github.dedis.popstellar.ui.qrcode; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED; +import static androidx.core.content.ContextCompat.checkSelfPermission; + import android.Manifest; import android.os.Bundle; import android.view.*; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -12,22 +15,14 @@ import androidx.camera.view.LifecycleCameraController; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; - import com.github.dedis.popstellar.databinding.QrScannerFragmentBinding; import com.github.dedis.popstellar.ui.PopViewModel; import com.google.mlkit.vision.barcode.*; import com.google.mlkit.vision.barcode.common.Barcode; - -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.Executor; - import timber.log.Timber; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED; -import static androidx.core.content.ContextCompat.checkSelfPermission; - public class QrScannerFragment extends Fragment { public static final String TAG = QrScannerFragment.class.getSimpleName(); @@ -66,7 +61,8 @@ public View onCreateView( scanningViewModel = scanningAction.obtainScannerViewModel(requireActivity(), popViewModel.getLaoId()); - if (scanningAction != ScanningAction.ADD_LAO_PARTICIPANT) { + if (scanningAction != ScanningAction.ADD_LAO_PARTICIPANT + && scanningAction != ScanningAction.ADD_POPCHA) { displayCounter(); } @@ -195,7 +191,7 @@ private void setupManualAdd() { binding.manualAddButton.setOnClickListener( v -> { - String input = binding.manualAddEditText.getText().toString(); + String input = Objects.requireNonNull(binding.manualAddEditText.getText()).toString(); onResult(input); }); } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/ScanningAction.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/ScanningAction.java index 3a688694e1..a43f5ad034 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/ScanningAction.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/ScanningAction.java @@ -4,14 +4,13 @@ import androidx.annotation.StringRes; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; - import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.ui.PopViewModel; import com.github.dedis.popstellar.ui.home.*; import com.github.dedis.popstellar.ui.lao.LaoActivity; import com.github.dedis.popstellar.ui.lao.event.rollcall.RollCallFragment; +import com.github.dedis.popstellar.ui.lao.popcha.PoPCHAHomeFragment; import com.github.dedis.popstellar.ui.lao.witness.WitnessingFragment; - import java.util.function.BiConsumer; import java.util.function.Function; @@ -66,7 +65,18 @@ public enum ScanningAction { (activity, unused) -> HomeActivity.obtainViewModel(activity), HomeActivity::obtainViewModel, (manager, unused) -> - HomeActivity.setCurrentFragment(manager, R.id.fragment_home, HomeFragment::new)); + HomeActivity.setCurrentFragment(manager, R.id.fragment_home, HomeFragment::new)), + ADD_POPCHA( + R.string.qrcode_scanning_add_popcha, + R.string.scanned_tokens, + R.string.popcha_add, + R.string.manual_popcha_hint, + R.string.popcha_scan_title, + LaoActivity::obtainPoPCHAViewModel, + LaoActivity::obtainViewModel, + (manager, unused) -> + LaoActivity.setCurrentFragment( + manager, R.id.fragment_popcha_home, PoPCHAHomeFragment::new)); @StringRes public final int instruction; @StringRes public final int scanTitle; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/MessageValidator.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/MessageValidator.java index 49283b4cc8..06f61e894b 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/MessageValidator.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/MessageValidator.java @@ -1,9 +1,11 @@ package com.github.dedis.popstellar.utility; +import static com.github.dedis.popstellar.model.qrcode.PoPCHAQRCode.*; + +import android.net.Uri; import com.github.dedis.popstellar.model.network.method.message.data.election.Vote; import com.github.dedis.popstellar.model.objects.*; import com.github.dedis.popstellar.model.objects.security.PublicKey; - import java.time.Instant; import java.util.*; import java.util.regex.Pattern; @@ -34,6 +36,12 @@ public static class MessageValidatorBuilder { // different devices can slightly vary public static final long VALID_FUTURE_DELAY = 120; + // Constants used for checking PoPCHA URLs + public static final String[] REQUIRED_ARGUMENTS = new String[] {CLIENT_ID, NONCE, REDIRECT_URI}; + public static final String VALID_RESPONSE_TYPE = "id_token"; + public static final String[] REQUIRED_SCOPES = new String[] {"openid", "profile"}; + public static final String[] VALID_RESPONSE_MODES = new String[] {"query", "fragment"}; + /** * Helper method to check that a LAO id is valid. * @@ -236,5 +244,42 @@ public MessageValidatorBuilder validUrl(String input) { } return this; } + + public MessageValidatorBuilder isValidPoPCHAUrl(String input, String laoId) { + // Check it's a valid url + MessageValidator.verify().validUrl(input); + + Uri uri = Uri.parse(input); + // Check required arguments are present + for (String arg : REQUIRED_ARGUMENTS) { + if (uri.getQueryParameter(arg) == null) { + throw new IllegalArgumentException( + String.format("Required argument %s is missing in the URL.", arg)); + } + } + // Check response type respects openid standards + String responseType = uri.getQueryParameter(RESPONSE_TYPE); + if (!responseType.equals(VALID_RESPONSE_TYPE)) { + throw new IllegalArgumentException("Invalid response type in the URL"); + } + // Check the scope contains all the required scopes + if (Arrays.stream(REQUIRED_SCOPES) + .anyMatch(name -> !uri.getQueryParameter("scope").contains(name))) { + throw new IllegalArgumentException("Invalid scope"); + } + // Check response mode is valid + String responseMode = uri.getQueryParameter(RESPONSE_MODE); + if (responseMode != null + && Arrays.stream(VALID_RESPONSE_MODES).noneMatch(responseMode::contains)) { + throw new IllegalArgumentException("Invalid response mode"); + } + // Check lao ID in login hint match the right laoID + String laoHint = uri.getQueryParameter(LOGIN_HINT); + if (!laoHint.equals(laoId)) { + throw new IllegalArgumentException(String.format("Invalid LAO ID %s", laoHint)); + } + + return this; + } } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/security/KeyManager.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/security/KeyManager.java index e64a1f770e..cacb5636e1 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/security/KeyManager.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/security/KeyManager.java @@ -1,7 +1,8 @@ package com.github.dedis.popstellar.utility.security; -import androidx.annotation.VisibleForTesting; +import static com.github.dedis.popstellar.di.KeysetModule.DeviceKeyset; +import androidx.annotation.VisibleForTesting; import com.github.dedis.popstellar.model.objects.RollCall; import com.github.dedis.popstellar.model.objects.Wallet; import com.github.dedis.popstellar.model.objects.security.*; @@ -11,20 +12,15 @@ import com.google.crypto.tink.*; import com.google.crypto.tink.integration.android.AndroidKeysetManager; import com.google.gson.*; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.Base64; - import javax.inject.Inject; import javax.inject.Singleton; - import timber.log.Timber; -import static com.github.dedis.popstellar.di.KeysetModule.DeviceKeyset; - /** Service managing keys and providing easy access to the main device key and PoP Tokens */ @Singleton public class KeyManager { @@ -88,6 +84,19 @@ public PoPToken getPoPToken(LaoView laoView, RollCall rollCall) throws KeyExcept return wallet.generatePoPToken(laoView.getId(), rollCall.getPersistentId()); } + /** + * Generate a long-term PoP Token for the given Lao - ClientID pair + * + * @param laoId to generate the PoP Token from + * @param clientId to generate the PoP Token from + * @return the generated PoP Token + * @throws KeyGenerationException if an error occurs during key generation + * @throws UninitializedWalletException if the wallet is not initialized with a seed + */ + public PoPToken getLongTermPoPToken(String laoId, String clientId) throws KeyException { + return wallet.generatePoPToken(laoId, Hash.hash(clientId)); + } + /** * Try to retrieve the user's PoPToken for the given Lao and RollCall. It will fail if the user * did not attend the roll call or if the token cannot be generated diff --git a/fe2-android/app/src/main/res/layout/popcha_home_fragment.xml b/fe2-android/app/src/main/res/layout/popcha_home_fragment.xml index 40f583caed..70ea784df8 100644 --- a/fe2-android/app/src/main/res/layout/popcha_home_fragment.xml +++ b/fe2-android/app/src/main/res/layout/popcha_home_fragment.xml @@ -14,10 +14,18 @@ android:layout_height="wrap_content" android:gravity="center" android:text="@string/popcha_header" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/popcha_scanner" app:layout_constraintTop_toTopOf="parent" tools:layout_editor_absoluteX="0dp" /> + +