Skip to content

Commit

Permalink
Repair broken OATH state by deleting corrupted credentials.
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Oct 5, 2023
1 parent 6589a9b commit cff013c
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 7 deletions.
16 changes: 14 additions & 2 deletions ykman/_cli/oath.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
parse_b32_key,
_format_cred_id,
)
from ..oath import is_steam, calculate_steam, is_hidden
from ..oath import is_steam, calculate_steam, is_hidden, delete_broken_credential
from ..settings import AppData


Expand Down Expand Up @@ -625,7 +625,19 @@ def code(ctx, show_hidden, query, single, password, remember):
_init_session(ctx, password, remember)

session = ctx.obj["session"]
entries = session.calculate_all()
try:
entries = session.calculate_all()
except ApduError as e:
if e.sw == SW.MEMORY_FAILURE:
logger.warning("Corrupted data in OATH accounts, attempting to fix")
if delete_broken_credential(session):
entries = session.calculate_all()
else:
logger.error("Unable to fix memory failure")
raise
else:
raise

creds = _search(entries.keys(), query, show_hidden)

if len(creds) == 1:
Expand Down
50 changes: 45 additions & 5 deletions ykman/oath.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,34 @@
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from yubikit.oath import OATH_TYPE
from yubikit.core.smartcard import ApduError, SW
from yubikit.oath import OathSession, Credential, OATH_TYPE
from time import time
from typing import Optional

import struct
import logging


logger = logging.getLogger(__name__)


STEAM_CHAR_TABLE = "23456789BCDFGHJKMNPQRTVWXY"


def is_hidden(credential):
def is_hidden(credential: Credential) -> bool:
"""Check if OATH credential is hidden."""
return credential.issuer == "_hidden"


def is_steam(credential):
def is_steam(credential: Credential) -> bool:
"""Check if OATH credential is steam."""
return credential.oath_type == OATH_TYPE.TOTP and credential.issuer == "Steam"


def calculate_steam(app, credential, timestamp=None):
def calculate_steam(
app: OathSession, credential: Credential, timestamp: Optional[int] = None
) -> str:
"""Calculate steam codes."""
timestamp = int(timestamp or time())
resp = app.calculate(credential.id, struct.pack(">q", timestamp // 30))
Expand All @@ -56,6 +65,37 @@ def calculate_steam(app, credential, timestamp=None):
return "".join(chars)


def is_in_fips_mode(app):
def is_in_fips_mode(app: OathSession) -> bool:
"""Check if OATH application is in FIPS mode."""
return app.locked


def delete_broken_credential(app: OathSession) -> bool:
"""Checks for credential in a broken state and deletes it."""
logger.debug("Probing for broken credentials")
creds = app.list_credentials()
broken = []
for c in creds:
if c.oath_type == OATH_TYPE.TOTP and not c.touch_required:
for i in range(5):
try:
app.calculate_code(c)
logger.debug(f"Credential appears OK: {c.id!r}")
break
except ApduError as e:
if e.sw == SW.MEMORY_FAILURE:
if i == 0:
logger.debug(f"Memory failure in: {c.id!r}")
continue
raise
else:
broken.append(c.id)
logger.warning(f"Credential appears to be broken: {c.id!r}")

if len(broken) == 1:
logger.info("Deleting broken credential")
app.delete_credential(broken[0])
return True

logger.warning(f"Requires a single broken credential, found {len(broken)}")
return False
1 change: 1 addition & 0 deletions yubikit/core/smartcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class AID(bytes, Enum):
class SW(IntEnum):
NO_INPUT_DATA = 0x6285
VERIFY_FAIL_NO_RETRY = 0x63C0
MEMORY_FAILURE = 0x6581
WRONG_LENGTH = 0x6700
SECURITY_CONDITION_NOT_SATISFIED = 0x6982
AUTH_METHOD_BLOCKED = 0x6983
Expand Down

0 comments on commit cff013c

Please sign in to comment.