Skip to content

Commit

Permalink
Merge PR #1302
Browse files Browse the repository at this point in the history
  • Loading branch information
elibon99 committed Dec 15, 2023
2 parents acc7ea9 + 0d3c7ef commit 624f4aa
Show file tree
Hide file tree
Showing 53 changed files with 4,759 additions and 262 deletions.
4 changes: 3 additions & 1 deletion helper/helper/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,9 @@ def ctap2(self):
or ( # SmartCardConnection can be used over NFC, or on 5.3 and later.
isinstance(self._connection, SmartCardConnection)
and (
self._transport == TRANSPORT.NFC or self._info.version >= (5, 3, 0)
self._transport == TRANSPORT.NFC
or self._info.version >= (5, 3, 0)
or self._info.version[0] == 3
)
)
)
Expand Down
115 changes: 87 additions & 28 deletions helper/helper/yubiotp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

from .base import RpcNode, action, child

from yubikit.core import NotSupportedError
from yubikit.core import NotSupportedError, CommandError
from yubikit.core.otp import modhex_encode, modhex_decode
from yubikit.yubiotp import (
YubiOtpSession,
SLOT,
Expand All @@ -25,7 +26,17 @@
YubiOtpSlotConfiguration,
StaticTicketSlotConfiguration,
)
from ykman.otp import generate_static_pw, format_csv
from yubikit.oath import parse_b32_key
from ykman.scancodes import KEYBOARD_LAYOUT, encode

from typing import Dict
import struct

_FAIL_MSG = (
"Failed to write to the YubiKey. Make sure the device does not "
"have restricted access"
)


class YubiOtpNode(RpcNode):
Expand Down Expand Up @@ -65,6 +76,29 @@ def one(self):
def two(self):
return SlotNode(self.session, SLOT.TWO)

@action(closes_child=False)
def serial_modhex(self, params, event, signal):
serial = params["serial"]
return dict(encoded=modhex_encode(b"\xff\x00" + struct.pack(b">I", serial)))

@action(closes_child=False)
def generate_static(self, params, event, signal):
layout, length = params["layout"], int(params["length"])
return dict(password=generate_static_pw(length, KEYBOARD_LAYOUT[layout]))

@action(closes_child=False)
def keyboard_layouts(self, params, event, signal):
return {layout.name: [sc for sc in layout.value] for layout in KEYBOARD_LAYOUT}

@action(closes_child=False)
def format_yubiotp_csv(self, params, even, signal):
serial = params["serial"]
public_id = modhex_decode(params["public_id"])
private_id = bytes.fromhex(params["private_id"])
key = bytes.fromhex(params["key"])

return dict(csv=format_csv(serial, public_id, private_id, key))


_CONFIG_TYPES = dict(
hmac_sha1=HmacSha1SlotConfiguration,
Expand Down Expand Up @@ -113,15 +147,18 @@ def _can_calculate(self, slot):

@action(condition=lambda self: self._maybe_configured(self.slot))
def delete(self, params, event, signal):
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
try:
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
except CommandError:
raise ValueError(_FAIL_MSG)

@action(condition=lambda self: self._can_calculate(self.slot))
def calculate(self, params, event, signal):
challenge = bytes.fromhex(params.pop("challenge"))
response = self.session.calculate_hmac_sha1(self.slot, challenge, event)
return dict(response=response)

def _apply_config(self, config, params):
def _apply_options(self, config, options):
for option in (
"serial_api_visible",
"serial_usb_visible",
Expand All @@ -140,47 +177,69 @@ def _apply_config(self, config, params):
"short_ticket",
"manual_update",
):
if option in params:
getattr(config, option)(params.pop(option))
if option in options:
getattr(config, option)(options.pop(option))

for option in ("tabs", "delay", "pacing", "strong_password"):
if option in params:
getattr(config, option)(*params.pop(option))
if option in options:
getattr(config, option)(*options.pop(option))

if "token_id" in params:
token_id, *args = params.pop("token_id")
if "token_id" in options:
token_id, *args = options.pop("token_id")
config.token_id(bytes.fromhex(token_id), *args)

return config

@action
def put(self, params, event, signal):
def _get_config(self, type, **kwargs):
config = None
for key in _CONFIG_TYPES:
if key in params:
if config is not None:
raise ValueError("Only one configuration type can be provided.")
config = _CONFIG_TYPES[key](
*(bytes.fromhex(arg) for arg in params.pop(key))

if type in _CONFIG_TYPES:
if type == "hmac_sha1":
config = _CONFIG_TYPES[type](bytes.fromhex(kwargs["key"]))
elif type == "hotp":
config = _CONFIG_TYPES[type](parse_b32_key(kwargs["key"]))
elif type == "static_password":
config = _CONFIG_TYPES[type](
encode(
kwargs["password"], KEYBOARD_LAYOUT[kwargs["keyboard_layout"]]
)
)
if config is None:
raise ValueError("No supported configuration type provided.")
self._apply_config(config, params)
self.session.put_configuration(
self.slot,
config,
params.pop("acc_code", None),
params.pop("cur_acc_code", None),
)
return dict()
elif type == "yubiotp":
config = _CONFIG_TYPES[type](
fixed=modhex_decode(kwargs["public_id"]),
uid=bytes.fromhex(kwargs["private_id"]),
key=bytes.fromhex(kwargs["key"]),
)
else:
raise ValueError("No supported configuration type provided.")
return config

@action
def put(self, params, event, signal):
type = params.pop("type")
options = params.pop("options", {})
args = params

config = self._get_config(type, **args)
self._apply_options(config, options)
try:
self.session.put_configuration(
self.slot,
config,
params.pop("acc_code", None),
params.pop("cur_acc_code", None),
)
return dict()
except CommandError:
raise ValueError(_FAIL_MSG)

@action(
condition=lambda self: self._state.version >= (2, 2, 0)
and self._maybe_configured(self.slot)
)
def update(self, params, event, signal):
config = UpdateConfiguration()
self._apply_config(config, params)
self._apply_options(config, params)
self.session.update_configuration(
self.slot,
config,
Expand Down
34 changes: 34 additions & 0 deletions integration_test/otp_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@Tags(['android', 'desktop', 'oath'])
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/app/views/keys.dart';

import 'utils/test_util.dart';

void main() {
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

group('OTP UI tests', () {
appTest('OTP menu items exist', (WidgetTester tester) async {
await tester.tap(find.byKey(otpAppDrawer));
await tester.shortWait();
});
});
}
12 changes: 11 additions & 1 deletion integration_test/piv_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ void main() {
const shortmanagementkey =
'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbccc';

appTest('Bad managementkey key', (WidgetTester tester) async {
appTest('Out of bounds managementkey key', (WidgetTester tester) async {
await tester.configurePiv();
await tester.shortWait();
await tester.tap(find.byKey(manageManagementKeyAction).hitTestable());
Expand All @@ -169,12 +169,22 @@ void main() {
await tester.longWait();
await tester.tap(find.byKey(saveButton).hitTestable());
await tester.longWait();
expect(tester.isTextButtonEnabled(saveButton), true);
// TODO assert that errorText and errorIcon are shown
});

appTest('Short managementkey key', (WidgetTester tester) async {
await tester.configurePiv();
await tester.shortWait();
await tester.tap(find.byKey(manageManagementKeyAction).hitTestable());
await tester.longWait();
// testing too short management key does not work
await tester.enterText(
find.byKey(newPinPukField).hitTestable(), shortmanagementkey);
await tester.longWait();
expect(tester.isTextButtonEnabled(saveButton), false);
});

appTest('Change managementkey key', (WidgetTester tester) async {
await tester.configurePiv();
await tester.shortWait();
Expand Down
3 changes: 2 additions & 1 deletion lib/app/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ enum Application {
String getDisplayName(AppLocalizations l10n) => switch (this) {
Application.oath => l10n.s_authenticator,
Application.fido => l10n.s_webauthn,
Application.piv => l10n.s_piv,
Application.piv => l10n.s_certificates,
Application.otp => l10n.s_slots,
_ => name.substring(0, 1).toUpperCase() + name.substring(1),
};

Expand Down
2 changes: 2 additions & 0 deletions lib/app/views/main_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import '../../core/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../fido/views/fido_screen.dart';
import '../../oath/views/oath_screen.dart';
import '../../otp/views/otp_screen.dart';
import '../../piv/views/piv_screen.dart';
import '../../widgets/custom_icons.dart';
import '../models.dart';
Expand Down Expand Up @@ -150,6 +151,7 @@ class MainPage extends ConsumerWidget {
Application.oath => OathScreen(data.node.path),
Application.fido => FidoScreen(data),
Application.piv => PivScreen(data.node.path),
Application.otp => OtpScreen(data.node.path),
_ => MessagePage(
header: l10n.s_app_not_supported,
message: l10n.l_app_not_supported_desc,
Expand Down
4 changes: 2 additions & 2 deletions lib/app/views/navigation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ extension on Application {
IconData get _icon => switch (this) {
Application.oath => Icons.supervisor_account_outlined,
Application.fido => Icons.security_outlined,
Application.otp => Icons.password_outlined,
Application.otp => Icons.touch_app_outlined,
Application.piv => Icons.approval_outlined,
Application.management => Icons.construction_outlined,
Application.openpgp => Icons.key_outlined,
Expand All @@ -99,7 +99,7 @@ extension on Application {
IconData get _filledIcon => switch (this) {
Application.oath => Icons.supervisor_account,
Application.fido => Icons.security,
Application.otp => Icons.password,
Application.otp => Icons.touch_app,
Application.piv => Icons.approval,
Application.management => Icons.construction,
Application.openpgp => Icons.key,
Expand Down
15 changes: 15 additions & 0 deletions lib/core/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,18 @@ class Version with _$Version implements Comparable<Version> {
}

final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');

enum Format {
base32('a-z2-7'),
hex('abcdef0123456789'),
modhex('cbdefghijklnrtuv');

final String allowedCharacters;

const Format(this.allowedCharacters);

bool isValid(String input) {
return RegExp('^[$allowedCharacters]+\$', caseSensitive: false)
.hasMatch(input);
}
}
5 changes: 5 additions & 0 deletions lib/desktop/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ import '../core/state.dart';
import '../fido/state.dart';
import '../management/state.dart';
import '../oath/state.dart';
import '../otp/state.dart';
import '../piv/state.dart';
import '../version.dart';
import 'devices.dart';
import 'fido/state.dart';
import 'management/state.dart';
import 'oath/state.dart';
import 'otp/state.dart';
import 'piv/state.dart';
import 'qr_scanner.dart';
import 'rpc.dart';
Expand Down Expand Up @@ -189,6 +191,7 @@ Future<Widget> initialize(List<String> argv) async {
Application.fido,
Application.piv,
Application.management,
Application.otp
])),
prefProvider.overrideWithValue(prefs),
rpcProvider.overrideWith((_) => rpcFuture),
Expand Down Expand Up @@ -226,6 +229,8 @@ Future<Widget> initialize(List<String> argv) async {
// PIV
pivStateProvider.overrideWithProvider(desktopPivState.call),
pivSlotsProvider.overrideWithProvider(desktopPivSlots.call),
// OTP
otpStateProvider.overrideWithProvider(desktopOtpState.call)
],
child: YubicoAuthenticatorApp(
page: Consumer(
Expand Down
Loading

0 comments on commit 624f4aa

Please sign in to comment.