#!/usr/bin/env python
import os
import os.path
import json
from typing import Any, Dict, Optional, Union
from .utils import Bytes, fromisoformat
from .token import SERIAL_LENGTH, Token, AbstractTokenFile
from .exceptions import ParseException, InvalidSeed, InvalidSerial
__all__ = [
'JSONTokenFile',
]
[docs]class JSONTokenFile(AbstractTokenFile):
"""
Handler for JSON file format
Example:
{
"digits": 6,
"exp_date": "2035-12-31",
"pin": 1234,
"period": 60,
"secret": [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25],
"serial": "000512377827",
"issuerInt": "myorg",
"label": "myaccount",
"type": "SecurID"
}
"""
filename: Optional[str]
token: Token
def __init__(
self,
filename: Optional[str] = None,
data: Union[bytes, bytearray, str, Dict[str, Any], None] = None,
token: Optional[Token] = None,
) -> None:
"""
:param filename: JSON file path
:param data: token as string in JSON format or as a dictionary
:param token: Token instance
"""
if token is not None:
self.filename = None
self.token = token
elif data is not None:
if isinstance(data, str):
data = bytes(data, 'ascii')
self.filename = None
self.token = self.json_decode_token(data)
elif filename is not None:
self.filename = os.path.expanduser(filename)
data = self.parse_file(self.filename)
self.token = self.json_decode_token(data)
[docs] @classmethod
def parse_file(cls, filename: str) -> bytes:
"""
Parse JSON file, return content as string
:param filename: JSON file path
"""
with open(filename, 'rb') as f:
return f.read()
@classmethod
def json_decode_token(cls, data: Union[Bytes, Dict[str, Any]]) -> Token:
try:
if isinstance(data, dict):
dct = data
else:
dct = json.loads(data)
token = Token(
digits=dct['digits'],
interval=dct['period'],
exp_date=fromisoformat(dct['exp_date'])
if dct.get('exp_date')
else None,
seed=bytes(dct['secret']),
serial=dct['serial'],
issuer=dct.get('issuerInt'),
label=dct.get('label'),
pin=dct.get('pin'),
)
return token
except json.decoder.JSONDecodeError as ex:
raise ParseException(ex)
[docs] def get_token(self, password: Optional[str] = None) -> Token:
"""
Return the Token instance
:param password: optional password for decrypting the token
"""
return self.token
[docs] def export_token(self) -> bytes:
"""
Export token as JSON
"""
if not self.token.seed:
raise InvalidSeed('Missing seed')
if not self.token.serial:
raise InvalidSerial('Missing serial')
if len(self.token.serial) != SERIAL_LENGTH:
raise InvalidSerial('Serial length != {}'.format(SERIAL_LENGTH))
data = {
'digits': self.token.digits,
'period': self.token.interval,
'exp_date': self.token.exp_date.isoformat() if self.token.exp_date else '',
'secret': [x for x in self.token.seed],
'serial': self.token.serial,
'type': 'SecurID',
}
if self.token.issuer is not None:
data['issuerInt'] = self.token.issuer
if self.token.label is not None:
data['label'] = self.token.label
if self.token.pin is not None:
data['pin'] = self.token.pin
j = json.dumps(data, sort_keys=True)
return bytes(j, 'ascii')