Skip to content

Commit

Permalink
Implement OTP pairing
Browse files Browse the repository at this point in the history
  • Loading branch information
ClassicOldSong committed Aug 27, 2024
1 parent 90b5f8f commit a4f6bd6
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 15 deletions.
84 changes: 73 additions & 11 deletions app/src/main/java/com/limelight/PcView.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@

import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
Expand All @@ -40,6 +42,8 @@
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.text.InputFilter;
import android.text.InputType;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
Expand All @@ -49,7 +53,9 @@
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
Expand Down Expand Up @@ -120,6 +126,7 @@ public void onConfigurationChanged(Configuration newConfig) {
private final static int TEST_NETWORK_ID = 10;
private final static int GAMESTREAM_EOL_ID = 11;
private final static int OPEN_MANAGEMENT_PAGE_ID = 20;
private final static int PAIR_ID_OTP = 21;

private void initializeViews() {
setContentView(R.layout.activity_pc_view);
Expand Down Expand Up @@ -362,11 +369,12 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn
menu.add(Menu.NONE, GAMESTREAM_EOL_ID, 2, getResources().getString(R.string.pcview_menu_eol));
}
else if (computer.details.pairState != PairState.PAIRED) {
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
menu.add(Menu.NONE, PAIR_ID_OTP, 1, getResources().getString(R.string.pcview_menu_pair_pc_otp));
menu.add(Menu.NONE, PAIR_ID, 2, getResources().getString(R.string.pcview_menu_pair_pc));
if (computer.details.nvidiaServer) {
menu.add(Menu.NONE, GAMESTREAM_EOL_ID, 2, getResources().getString(R.string.pcview_menu_eol));
menu.add(Menu.NONE, GAMESTREAM_EOL_ID, 3, getResources().getString(R.string.pcview_menu_eol));
} else {
menu.add(Menu.NONE, OPEN_MANAGEMENT_PAGE_ID, 2, getResources().getString(R.string.pcview_menu_open_management_page));
menu.add(Menu.NONE, OPEN_MANAGEMENT_PAGE_ID, 3, getResources().getString(R.string.pcview_menu_open_management_page));
}
}
else {
Expand Down Expand Up @@ -397,7 +405,7 @@ public void onContextMenuClosed(Menu menu) {
startComputerUpdates();
}

private void doPair(final ComputerDetails computer) {
private void doPair(final ComputerDetails computer, String otp, String passphrase) {
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
Expand Down Expand Up @@ -427,16 +435,25 @@ public void run() {
success = true;
}
else {
final String pinStr = PairingManager.generatePinString();
String pinStr = otp;
if (pinStr == null) {
pinStr = PairingManager.generatePinString();
}

// Spin the dialog off in a thread because it blocks
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr+"\n\n"+
getResources().getString(R.string.pair_pairing_help), false);
if (passphrase == null) {
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr+"\n\n"+
getResources().getString(R.string.pair_pairing_help), false);
} else {
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_otp_pairing_msg)+"\n\n"+
getResources().getString(R.string.pair_otp_pairing_help), false);
}

PairingManager pm = httpConn.getPairingManager();

PairState pairState = pm.pair(httpConn.getServerInfo(true), pinStr);
PairState pairState = pm.pair(httpConn.getServerInfo(true), pinStr, passphrase);
if (pairState == PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin);
}
Expand Down Expand Up @@ -502,6 +519,47 @@ public void run() {
}).start();
}

private void doOTPPair(final ComputerDetails computer) {
Context context = PcView.this;

LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(50, 40, 50, 40);

final EditText otpInput = new EditText(context);
otpInput.setHint("PIN");
otpInput.setInputType(InputType.TYPE_CLASS_NUMBER);
otpInput.setFilters(new InputFilter[] { new InputFilter.LengthFilter(4) });

final EditText passphraseInput = new EditText(context);
passphraseInput.setHint(getString(R.string.pair_passphrase_hint));
passphraseInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);

layout.addView(otpInput);
layout.addView(passphraseInput);

AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
dialogBuilder.setTitle(R.string.pcview_menu_pair_pc_otp);
dialogBuilder.setView(layout);

dialogBuilder.setPositiveButton(getString(R.string.proceed), (dialog, which) -> {
String pin = otpInput.getText().toString();
String passphrase = passphraseInput.getText().toString();
if (pin.length() != 4) {
Toast.makeText(context, getString(R.string.pair_pin_length_msg), Toast.LENGTH_SHORT).show();
return;
}
if (passphrase.length() < 4 ) {
Toast.makeText(context, getString(R.string.pair_passphrase_length_msg), Toast.LENGTH_SHORT).show();
return;
}
doPair(computer, pin, passphrase);
});

dialogBuilder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.dismiss());
dialogBuilder.create().show();
}

private void doWakeOnLan(final ComputerDetails computer) {
if (computer.state == ComputerDetails.State.ONLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
Expand Down Expand Up @@ -611,7 +669,11 @@ public boolean onContextItemSelected(MenuItem item) {
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case PAIR_ID:
doPair(computer.details);
doPair(computer.details, null, null);
return true;

case PAIR_ID_OTP:
doOTPPair(computer.details);
return true;

case UNPAIR_ID:
Expand Down Expand Up @@ -773,7 +835,7 @@ public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
doPair(computer.details, null, null);
} else {
doAppList(computer.details, false, false);
}
Expand Down
30 changes: 26 additions & 4 deletions app/src/main/java/com/limelight/nvstream/http/PairingManager.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.limelight.nvstream.http;

import android.widget.Toast;

import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.engines.AESLightEngine;
import org.bouncycastle.crypto.params.KeyParameter;
Expand Down Expand Up @@ -182,7 +184,7 @@ public X509Certificate getPairedCert() {
return serverCert;
}

public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
public PairState pair(String serverInfo, String pin, String passphrase) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo;

int serverMajorVersion = http.getServerMajorVersion(serverInfo);
Expand All @@ -201,12 +203,32 @@ public PairState pair(String serverInfo, String pin) throws IOException, XmlPull

// Combine the salt and pin, then create an AES key from them
byte[] aesKey = generateAesKey(hashAlgo, saltPin(salt, pin));

String saltStr = bytesToHex(salt);

String pairingArguments = "phrase=getservercert&salt="+
saltStr+"&clientcert="+bytesToHex(pemCertBytes);

if (passphrase != null) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
String plainText = pin + saltStr + passphrase;
byte[] hash = digest.digest(plainText.getBytes());

StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02X", b));
}

pairingArguments += "&otpauth=" + hexString;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

// Send the salt and get the server cert. This doesn't have a read timeout
// because the user must enter the PIN before the server responds
String getCert = http.executePairingCommand("phrase=getservercert&salt="+
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
false);
String getCert = http.executePairingCommand(pairingArguments, false);
if (!NvHTTP.getXmlString(getCert, "paired", true).equals("1")) {
return PairState.FAILED;
}
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<!-- PC view menu entries -->
<string name="pcview_menu_app_list"> 浏览游戏列表 </string>
<string name="pcview_menu_pair_pc"> 和电脑配对 </string>
<string name="pcview_menu_pair_pc_otp"> OTP配对 </string>
<string name="pcview_menu_unpair_pc"> 取消配对 </string>
<string name="pcview_menu_send_wol"> 发送 Wake-On-LAN 请求 </string>
<string name="pcview_menu_delete_pc"> 删除电脑 </string>
Expand All @@ -24,9 +25,14 @@
<string name="pair_pc_ingame">电脑正在游戏中,在配对之前你必须先退出游戏。</string>
<string name="pair_pairing_title"> 配对中 </string>
<string name="pair_pairing_msg"> 请在目标电脑上输入以下PIN码: </string>
<string name="pair_otp_pairing_msg">正在配对,请稍候…</string>
<string name="pair_otp_pairing_help">OTP 配对仅支持 Apollo,其他服务端请在 WebUI 上手动输入刚刚提供的PIN码。</string>
<string name="pair_incorrect_pin"> PIN码错误 </string>
<string name="pair_fail"> 配对失败 </string>
<string name="pair_already_in_progress"> 配对中,请稍候 </string>
<string name="pair_passphrase_hint">口令</string>
<string name="pair_pin_length_msg">Pin必须为4个数字</string>
<string name="pair_passphrase_length_msg">口令至少需要4位</string>
<!-- WOL messages -->
<string name="wol_pc_online"> 电脑在线中 </string>
<string name="wol_no_mac">无法唤醒电脑,因为没有存储的 MAC 地址</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<string name="pcview_menu_header_unknown">Refreshing</string>
<string name="pcview_menu_app_list">View All Apps</string>
<string name="pcview_menu_pair_pc">Pair with PC</string>
<string name="pcview_menu_pair_pc_otp">OTP Pair</string>
<string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string>
Expand All @@ -42,9 +43,14 @@
<string name="pair_pairing_title">Pairing</string>
<string name="pair_pairing_msg">Please enter the following PIN on the target PC:</string>
<string name="pair_pairing_help">If your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.</string>
<string name="pair_otp_pairing_msg">Pairing, please wait…</string>
<string name="pair_otp_pairing_help">OTP pairing is only available with Apollo, enter the PIN you just provided on the web UI manually if you\'re using other host software.</string>
<string name="pair_incorrect_pin">Incorrect PIN</string>
<string name="pair_fail">Pairing failed</string>
<string name="pair_already_in_progress">Pairing already in progress</string>
<string name="pair_passphrase_hint">Passphrase</string>
<string name="pair_pin_length_msg">Pin must be 4 digits long</string>
<string name="pair_passphrase_length_msg">Passphrase must be at least 4 characters long</string>

<!-- WOL messages -->
<string name="wol_pc_online">Computer is online</string>
Expand Down

0 comments on commit a4f6bd6

Please sign in to comment.