Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor otp integration #173

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
Expand All @@ -48,11 +49,10 @@
import com.koushikdutta.async.future.FutureCallback;
import com.loopj.android.http.AsyncHttpResponseHandler;

import net.bierbaumer.otp_authenticator.TOTPHelper;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

import es.wolfi.app.ResponseHandlers.CredentialAddFileResponseHandler;
Expand All @@ -70,6 +70,8 @@
import es.wolfi.passman.API.Vault;
import es.wolfi.utils.FileUtils;
import es.wolfi.utils.ProgressUtils;
import es.wolfi.utils.otp.HashingAlgorithm;
import es.wolfi.utils.otp.TOTPHelper;


/**
Expand Down Expand Up @@ -99,6 +101,7 @@ public class CredentialAddFragment extends Fragment implements View.OnClickListe
EditText otp_issuer;
TextView credential_otp;
ProgressBar otp_progress;
Spinner otp_algorithm_spinner;
Spinner customFieldType;

private OnCredentialFragmentInteraction mListener;
Expand All @@ -111,7 +114,7 @@ public class CredentialAddFragment extends Fragment implements View.OnClickListe
private Handler handler = null;
private Runnable otp_refresh = null;
private String otp_qr_uri = "";
private String otp_algorithm = "SHA1";
private HashingAlgorithm otp_algorithm = HashingAlgorithm.SHA1;
private String otp_type = "totp";

public CredentialAddFragment() {
Expand Down Expand Up @@ -169,6 +172,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
otp_secret = view.findViewById(R.id.edit_credential_otp_secret);
otp_digits = view.findViewById(R.id.edit_credential_otp_digits);
otp_period = view.findViewById(R.id.edit_credential_otp_period);
otp_algorithm_spinner = view.findViewById(R.id.edit_credential_otp_algorithm);
otp_label = view.findViewById(R.id.otp_label);
otp_issuer = view.findViewById(R.id.otp_issuer);
credential_otp = view.findViewById(R.id.credential_otp);
Expand Down Expand Up @@ -197,8 +201,12 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
customFieldsListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
customFieldsListRecyclerView.setAdapter(cfed);

ArrayAdapter<String> adapter = new ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item, HashingAlgorithm.hashingAlgorithmsFriendlyArray);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
otp_algorithm_spinner.setAdapter(adapter);

handler = new Handler();
otp_refresh = TOTPHelper.runAndUpdate(handler, otp_progress, credential_otp, otp_digits, otp_period, otp_secret);
otp_refresh = TOTPHelper.runAndUpdate(handler, otp_progress, credential_otp, otp_digits, otp_period, otp_secret, otp_algorithm_spinner);
}

@Override
Expand Down Expand Up @@ -266,7 +274,11 @@ private void setOTPValuesFromJSON(JSONObject otpObj) {
otp_type = otpObj.getString("type");
}
if (otpObj.has("algorithm")) {
otp_algorithm = otpObj.getString("algorithm");
otp_algorithm = HashingAlgorithm.SHA1;
if (otpObj.has("algorithm")) {
otp_algorithm = HashingAlgorithm.fromStringOrSha1(otpObj.getString("algorithm"));
}
otp_algorithm_spinner.setSelection(Arrays.asList(HashingAlgorithm.hashingAlgorithmsFriendlyArray).indexOf(otp_algorithm.getFriendlyName()));
}
if (otpObj.has("qr_uri")) {
otp_qr_uri = otpObj.getString("qr_uri");
Expand Down Expand Up @@ -396,7 +408,7 @@ public void onClick(View view) {
otp_label,
otp_issuer,
otp_qr_uri,
otp_algorithm,
otp_algorithm_spinner.getSelectedItem().toString(),
otp_type
);
this.credential.setOtp(otpObj.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@

import com.google.android.material.floatingactionbutton.FloatingActionButton;

import net.bierbaumer.otp_authenticator.TOTPHelper;

import org.json.JSONException;
import org.json.JSONObject;

Expand All @@ -59,6 +57,8 @@
import es.wolfi.passman.API.SharingACL;
import es.wolfi.passman.API.Vault;
import es.wolfi.utils.IconUtils;
import es.wolfi.utils.otp.HashingAlgorithm;
import es.wolfi.utils.otp.TOTPHelper;


/**
Expand Down Expand Up @@ -271,18 +271,25 @@ public void onClick(View view) {
JSONObject otpObj = new JSONObject(credential.getOtp());
if (otpObj.has("secret") && otpObj.getString("secret").length() > 4) {
String otpSecret = otpObj.getString("secret");

int otpDigits = 6;
if (otpObj.has("digits")) {
otpDigits = otpObj.getInt("digits");
}

int otpPeriod = 30;
if (otpObj.has("period")) {
otpPeriod = otpObj.getInt("period");
}

HashingAlgorithm hashingAlgorithm = HashingAlgorithm.SHA1;
if (otpObj.has("algorithm")) {
hashingAlgorithm = HashingAlgorithm.fromStringOrSha1(otpObj.getString("algorithm"));
}

int finalOtpDigits = otpDigits;
int finalOtpPeriod = otpPeriod;
otp_refresh = TOTPHelper.run(handler, otp_progress, otp.getTextView(), finalOtpDigits, finalOtpPeriod, otpSecret);
otp_refresh = TOTPHelper.run(handler, otp_progress, otp.getTextView(), finalOtpDigits, finalOtpPeriod, otpSecret, hashingAlgorithm);
}
} catch (JSONException e) {
e.printStackTrace();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
Expand All @@ -48,11 +49,10 @@
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.loopj.android.http.AsyncHttpResponseHandler;

import net.bierbaumer.otp_authenticator.TOTPHelper;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;

Expand All @@ -72,6 +72,8 @@
import es.wolfi.passman.API.Vault;
import es.wolfi.utils.FileUtils;
import es.wolfi.utils.ProgressUtils;
import es.wolfi.utils.otp.HashingAlgorithm;
import es.wolfi.utils.otp.TOTPHelper;


/**
Expand Down Expand Up @@ -101,7 +103,7 @@ public class CredentialEditFragment extends Fragment implements View.OnClickList
EditText otp_issuer;
TextView credential_otp;
ProgressBar otp_progress;

Spinner otp_algorithm_spinner;
Spinner customFieldType;

private Credential credential;
Expand All @@ -113,7 +115,7 @@ public class CredentialEditFragment extends Fragment implements View.OnClickList
private Handler handler = null;
private Runnable otp_refresh = null;
private String otp_qr_uri = "";
private String otp_algorithm = "SHA1";
private HashingAlgorithm otp_algorithm = HashingAlgorithm.SHA1;
private String otp_type = "totp";

public CredentialEditFragment() {
Expand Down Expand Up @@ -181,6 +183,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
otp_secret = view.findViewById(R.id.edit_credential_otp_secret);
otp_digits = view.findViewById(R.id.edit_credential_otp_digits);
otp_period = view.findViewById(R.id.edit_credential_otp_period);
otp_algorithm_spinner = view.findViewById(R.id.edit_credential_otp_algorithm);
otp_label = view.findViewById(R.id.otp_label);
otp_issuer = view.findViewById(R.id.otp_issuer);
credential_otp = view.findViewById(R.id.credential_otp);
Expand Down Expand Up @@ -208,6 +211,10 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
customFieldsListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
customFieldsListRecyclerView.setAdapter(cfed);

ArrayAdapter<String> adapter = new ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item, HashingAlgorithm.hashingAlgorithmsFriendlyArray);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
otp_algorithm_spinner.setAdapter(adapter);

label.setText(this.credential.getLabel());
user.setText(this.credential.getUsername());
password.setText(this.credential.getPassword());
Expand All @@ -226,7 +233,7 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
}

handler = new Handler();
otp_refresh = TOTPHelper.runAndUpdate(handler, otp_progress, credential_otp, otp_digits, otp_period, otp_secret);
otp_refresh = TOTPHelper.runAndUpdate(handler, otp_progress, credential_otp, otp_digits, otp_period, otp_secret, otp_algorithm_spinner);
} catch (JSONException e) {
e.printStackTrace();
}
Expand Down Expand Up @@ -293,7 +300,11 @@ private void setOTPValuesFromJSON(JSONObject otpObj) {
otp_type = otpObj.getString("type");
}
if (otpObj.has("algorithm")) {
otp_algorithm = otpObj.getString("algorithm");
otp_algorithm = HashingAlgorithm.SHA1;
if (otpObj.has("algorithm")) {
otp_algorithm = HashingAlgorithm.fromStringOrSha1(otpObj.getString("algorithm"));
}
otp_algorithm_spinner.setSelection(Arrays.asList(HashingAlgorithm.hashingAlgorithmsFriendlyArray).indexOf(otp_algorithm.getFriendlyName()));
}
if (otpObj.has("qr_uri")) {
otp_qr_uri = otpObj.getString("qr_uri");
Expand Down Expand Up @@ -461,7 +472,7 @@ public void onClick(View view) {
otp_label,
otp_issuer,
otp_qr_uri,
otp_algorithm,
otp_algorithm_spinner.getSelectedItem().toString(),
otp_type
);
this.credential.setOtp(otpObj.toString());
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/es/wolfi/utils/otp/CodeGenerationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Passman Android App
*
* @copyright Copyright (c) 2021, Sander Brand ([email protected])
* @copyright Copyright (c) 2021, Marcos Zuriaga Miguel ([email protected])
* @copyright Copyright (c) 2024, Timo Triebensky ([email protected])
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* <p>
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package es.wolfi.utils.otp;

public class CodeGenerationException extends Exception {
public CodeGenerationException(String message, Throwable cause) {
super(message, cause);
}
}
111 changes: 111 additions & 0 deletions app/src/main/java/es/wolfi/utils/otp/CodeGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Passman Android App
*
* @copyright Copyright (c) 2021, Sander Brand ([email protected])
* @copyright Copyright (c) 2021, Marcos Zuriaga Miguel ([email protected])
* @copyright Copyright (c) 2024, Timo Triebensky ([email protected])
* @license GNU AGPL version 3 or any later version
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* <p>
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package es.wolfi.utils.otp;

import org.apache.commons.codec.binary.Base32;

import java.security.InvalidKeyException;
import java.security.InvalidParameterException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class CodeGenerator {
private final HashingAlgorithm algorithm;
private final int digits;
private final int period;

public CodeGenerator() {
this(HashingAlgorithm.SHA1, 6, 30);
}

public CodeGenerator(HashingAlgorithm algorithm, int digits, int period) {
if (algorithm == null) {
throw new InvalidParameterException("HashingAlgorithm must not be null.");
}
if (digits < 1) {
throw new InvalidParameterException("Number of digits must be higher than 0.");
}
if (period < 1) {
throw new InvalidParameterException("Time step (period) must be higher than 0.");
}

this.algorithm = algorithm;
this.digits = digits;
this.period = period;
}

public String generate(String key) throws CodeGenerationException {
try {
// Get the current number of seconds since the epoch and
// calculate the number of time periods passed.
// see https://datatracker.ietf.org/doc/html/rfc6238#section-4.2
long counter = Math.floorDiv(System.currentTimeMillis() / 1000, period);

byte[] hash = generateHash(key, counter);
return getDigitsFromHash(hash);
} catch (Exception e) {
throw new CodeGenerationException("Failed to generate code. See nested exception.", e);
}
}

/**
* Generate a HMAC-SHA hash of the given key and counter number.
*/
private byte[] generateHash(String key, long counter) throws InvalidKeyException, NoSuchAlgorithmException {
byte[] data = new byte[8];
long value = counter;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}

Base32 codec = new Base32();
byte[] decodedKey = codec.decode(key);
SecretKeySpec signKey = new SecretKeySpec(decodedKey, algorithm.getHmacAlgorithm());
Mac mac = Mac.getInstance(algorithm.getHmacAlgorithm());
mac.init(signKey);

return mac.doFinal(data);
}

/**
* Get the n-digit code for a given hash.
*/
private String getDigitsFromHash(byte[] hash) {
int offset = hash[hash.length - 1] & 0xF;

long truncatedHash = 0;

for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
truncatedHash |= (hash[offset + i] & 0xFF);
}

truncatedHash &= 0x7FFFFFFF;
truncatedHash %= (long) Math.pow(10, digits);

// Left pad with 0s for a n-digit code
return String.format("%0" + digits + "d", truncatedHash);
}
}
Loading