2019-04-25 14:38:56 +00:00
|
|
|
import hashlib
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from . import consts, crypto, helpers, prng
|
|
|
|
from .norcow import Norcow
|
|
|
|
from .pin_log import PinLog
|
|
|
|
|
|
|
|
|
|
|
|
class Storage:
|
|
|
|
def __init__(self):
|
|
|
|
self.initialized = False
|
|
|
|
self.unlocked = False
|
|
|
|
self.dek = None
|
|
|
|
self.sak = None
|
|
|
|
self.nc = Norcow()
|
|
|
|
self.pin_log = PinLog(self.nc)
|
|
|
|
|
|
|
|
def init(self, hardware_salt: bytes = b""):
|
|
|
|
"""
|
|
|
|
Initializes storage. Normally we would check if EDEK is already present,
|
|
|
|
but we simplify things in the python version and suppose we are starting
|
|
|
|
a new storage each time.
|
|
|
|
"""
|
|
|
|
self.nc.init()
|
|
|
|
self.initialized = True
|
|
|
|
self.hw_salt_hash = hashlib.sha256(hardware_salt).digest()
|
|
|
|
|
|
|
|
edek_esak_pvc = self.nc.get(consts.EDEK_ESEK_PVC_KEY)
|
|
|
|
if not edek_esak_pvc:
|
|
|
|
self._init_pin()
|
|
|
|
|
|
|
|
def _init_pin(self):
|
|
|
|
"""
|
|
|
|
Initalizes PIN counters, generates random
|
|
|
|
Data Encryption Key and Storage Authentication Key
|
|
|
|
"""
|
|
|
|
self.dek = prng.random_buffer(consts.DEK_SIZE)
|
|
|
|
self.sak = prng.random_buffer(consts.SAK_SIZE)
|
|
|
|
|
|
|
|
self.nc.set(consts.SAT_KEY, crypto.init_hmacs(self.sak))
|
2020-08-14 16:54:44 +00:00
|
|
|
self._set_encrypt(consts.VERSION_KEY, consts.NORCOW_VERSION)
|
|
|
|
self.nc.set(consts.UNAUTH_VERSION_KEY, consts.NORCOW_VERSION)
|
2019-10-23 15:50:46 +00:00
|
|
|
self.nc.set(consts.STORAGE_UPGRADED_KEY, consts.FALSE_WORD)
|
2019-04-25 14:38:56 +00:00
|
|
|
self.pin_log.init()
|
2019-10-31 13:44:39 +00:00
|
|
|
self._set_wipe_code(consts.WIPE_CODE_EMPTY)
|
2019-04-25 14:38:56 +00:00
|
|
|
self._set_pin(consts.PIN_EMPTY)
|
|
|
|
self.unlocked = False
|
|
|
|
|
2020-08-14 16:54:44 +00:00
|
|
|
def _set_pin(self, pin: str):
|
2019-04-25 14:38:56 +00:00
|
|
|
random_salt = prng.random_buffer(consts.PIN_SALT_SIZE)
|
|
|
|
salt = self.hw_salt_hash + random_salt
|
|
|
|
kek, keiv = crypto.derive_kek_keiv(salt, pin)
|
|
|
|
|
|
|
|
# Encrypted Data Encryption Key and Encrypted Storage Authentication Key
|
|
|
|
edek_esak, tag = crypto.chacha_poly_encrypt(kek, keiv, self.dek + self.sak)
|
|
|
|
# Pin Verification Code
|
|
|
|
pvc = tag[: consts.PVC_SIZE]
|
|
|
|
|
|
|
|
self.nc.set(consts.EDEK_ESEK_PVC_KEY, random_salt + edek_esak + pvc)
|
|
|
|
if pin == consts.PIN_EMPTY:
|
|
|
|
self._set_bool(consts.PIN_NOT_SET_KEY, True)
|
|
|
|
else:
|
|
|
|
self._set_bool(consts.PIN_NOT_SET_KEY, False)
|
|
|
|
|
2020-08-14 16:54:44 +00:00
|
|
|
def _set_wipe_code(self, wipe_code: str):
|
2019-10-31 13:44:39 +00:00
|
|
|
if wipe_code == consts.PIN_EMPTY:
|
|
|
|
wipe_code = consts.WIPE_CODE_EMPTY
|
2020-08-14 16:54:44 +00:00
|
|
|
wipe_code_bytes = wipe_code.encode()
|
2019-10-31 13:44:39 +00:00
|
|
|
salt = prng.random_buffer(consts.WIPE_CODE_SALT_SIZE)
|
|
|
|
tag = crypto._hmac(salt, wipe_code_bytes)[: consts.WIPE_CODE_TAG_SIZE]
|
|
|
|
self.nc.set(consts.WIPE_CODE_DATA_KEY, wipe_code_bytes + salt + tag)
|
|
|
|
|
2019-04-25 14:38:56 +00:00
|
|
|
def wipe(self):
|
|
|
|
self.nc.wipe()
|
|
|
|
self._init_pin()
|
|
|
|
|
2020-08-14 16:54:44 +00:00
|
|
|
def check_pin(self, pin: str) -> bool:
|
2019-04-25 14:38:56 +00:00
|
|
|
self.pin_log.write_attempt()
|
|
|
|
|
|
|
|
data = self.nc.get(consts.EDEK_ESEK_PVC_KEY)
|
|
|
|
salt = self.hw_salt_hash + data[: consts.PIN_SALT_SIZE]
|
|
|
|
edek_esak = data[consts.PIN_SALT_SIZE : -consts.PVC_SIZE]
|
|
|
|
pvc = data[-consts.PVC_SIZE :]
|
|
|
|
|
|
|
|
try:
|
|
|
|
dek, sak = crypto.decrypt_edek_esak(pin, salt, edek_esak, pvc)
|
|
|
|
self.pin_log.write_success()
|
|
|
|
self.dek = dek
|
|
|
|
self.sak = sak
|
|
|
|
return True
|
|
|
|
except crypto.InvalidPinError:
|
|
|
|
fails = self.pin_log.get_failures_count()
|
|
|
|
if fails >= consts.PIN_MAX_TRIES:
|
|
|
|
self.wipe()
|
|
|
|
return False
|
|
|
|
|
|
|
|
def lock(self) -> None:
|
|
|
|
self.unlocked = False
|
|
|
|
|
2020-08-14 16:54:44 +00:00
|
|
|
def unlock(self, pin: str) -> bool:
|
2019-04-25 14:38:56 +00:00
|
|
|
if not self.initialized or not self.check_pin(pin):
|
|
|
|
return False
|
|
|
|
|
|
|
|
version = self._decrypt(consts.VERSION_KEY)
|
|
|
|
if version != consts.NORCOW_VERSION:
|
|
|
|
return False
|
|
|
|
|
|
|
|
self.unlocked = True
|
|
|
|
return True
|
|
|
|
|
|
|
|
def has_pin(self) -> bool:
|
|
|
|
val = self.nc.get(consts.PIN_NOT_SET_KEY)
|
|
|
|
return val != consts.TRUE_BYTE
|
|
|
|
|
|
|
|
def get_pin_rem(self) -> int:
|
|
|
|
return consts.PIN_MAX_TRIES - self.pin_log.get_failures_count()
|
|
|
|
|
2020-08-14 16:54:44 +00:00
|
|
|
def change_pin(self, oldpin: str, newpin: str) -> bool:
|
|
|
|
if not self.initialized or not self.unlocked:
|
2019-04-25 14:38:56 +00:00
|
|
|
return False
|
|
|
|
if not self.check_pin(oldpin):
|
|
|
|
return False
|
|
|
|
self._set_pin(newpin)
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get(self, key: int) -> bytes:
|
|
|
|
app = key >> 8
|
|
|
|
if not self.initialized or consts.is_app_private(app):
|
|
|
|
raise RuntimeError("Storage not initialized or app is private")
|
|
|
|
if not self.unlocked and not consts.is_app_public(app):
|
|
|
|
# public fields can be read from an unlocked device
|
|
|
|
raise RuntimeError("Storage locked")
|
|
|
|
if consts.is_app_public(app):
|
2019-12-04 14:23:54 +00:00
|
|
|
value = self.nc.get(key)
|
|
|
|
else:
|
|
|
|
value = self._get_encrypted(key)
|
|
|
|
if value is False:
|
|
|
|
raise RuntimeError("Failed to find key in storage.")
|
|
|
|
return value
|
2019-04-25 14:38:56 +00:00
|
|
|
|
|
|
|
def set(self, key: int, val: bytes) -> bool:
|
|
|
|
app = key >> 8
|
|
|
|
self._check_lock(app)
|
|
|
|
if consts.is_app_public(app):
|
|
|
|
return self.nc.set(key, val)
|
|
|
|
return self._set_encrypt(key, val)
|
|
|
|
|
|
|
|
def set_counter(self, key: int, val: int):
|
|
|
|
app = key >> 8
|
|
|
|
if not consts.is_app_public(app):
|
|
|
|
raise RuntimeError("Counter can be set only for public items")
|
2021-03-11 13:00:51 +00:00
|
|
|
|
|
|
|
if val > consts.UINT32_MAX:
|
|
|
|
raise RuntimeError("Failed to set value in storage.")
|
|
|
|
|
2019-04-25 14:38:56 +00:00
|
|
|
counter = val.to_bytes(4, sys.byteorder) + bytearray(
|
|
|
|
b"\xFF" * consts.COUNTER_TAIL_SIZE
|
|
|
|
)
|
|
|
|
self.set(key, counter)
|
|
|
|
|
|
|
|
def next_counter(self, key: int) -> int:
|
|
|
|
app = key >> 8
|
|
|
|
self._check_lock(app)
|
|
|
|
|
2019-12-04 14:23:54 +00:00
|
|
|
current = self.nc.get(key)
|
2019-04-25 14:38:56 +00:00
|
|
|
if current is False:
|
|
|
|
self.set_counter(key, 0)
|
|
|
|
return 0
|
|
|
|
|
|
|
|
base = int.from_bytes(current[:4], sys.byteorder)
|
|
|
|
tail = helpers.to_int_by_words(current[4:])
|
|
|
|
tail_count = "{0:064b}".format(tail).count("0")
|
|
|
|
increased_count = base + tail_count + 1
|
2021-03-11 13:00:51 +00:00
|
|
|
if increased_count > consts.UINT32_MAX:
|
|
|
|
raise RuntimeError("Failed to set value in storage.")
|
2019-04-25 14:38:56 +00:00
|
|
|
|
|
|
|
if tail_count == consts.COUNTER_MAX_TAIL:
|
|
|
|
self.set_counter(key, increased_count)
|
|
|
|
return increased_count
|
|
|
|
|
|
|
|
self.set(
|
|
|
|
key,
|
|
|
|
current[:4]
|
|
|
|
+ helpers.to_bytes_by_words(tail >> 1, consts.COUNTER_TAIL_SIZE),
|
|
|
|
)
|
|
|
|
return increased_count
|
|
|
|
|
|
|
|
def delete(self, key: int) -> bool:
|
|
|
|
app = key >> 8
|
|
|
|
self._check_lock(app)
|
|
|
|
ret = self.nc.delete(key)
|
|
|
|
if consts.is_app_protected(app):
|
|
|
|
sat = self._calculate_authentication_tag()
|
|
|
|
self.nc.set(consts.SAT_KEY, sat)
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def _check_lock(self, app: int):
|
|
|
|
if not self.initialized or consts.is_app_private(app):
|
|
|
|
raise RuntimeError("Storage not initialized or app is private")
|
|
|
|
if not self.unlocked and not consts.is_app_lock_writable(app):
|
|
|
|
raise RuntimeError("Storage locked and app is not public-writable")
|
|
|
|
|
|
|
|
def _get_encrypted(self, key: int) -> bytes:
|
|
|
|
if not consts.is_app_protected(key):
|
|
|
|
raise RuntimeError("Only protected values are encrypted")
|
|
|
|
sat = self.nc.get(consts.SAT_KEY)
|
|
|
|
if not sat:
|
|
|
|
raise RuntimeError("SAT not found")
|
|
|
|
if sat != self._calculate_authentication_tag():
|
|
|
|
raise RuntimeError("Storage authentication tag mismatch")
|
|
|
|
return self._decrypt(key)
|
|
|
|
|
|
|
|
def _decrypt(self, key: int) -> bytes:
|
|
|
|
data = self.nc.get(key)
|
|
|
|
iv = data[: consts.CHACHA_IV_SIZE]
|
|
|
|
# cipher text with MAC
|
|
|
|
tag = data[
|
|
|
|
consts.CHACHA_IV_SIZE : consts.CHACHA_IV_SIZE + consts.POLY1305_MAC_SIZE
|
|
|
|
]
|
|
|
|
ciphertext = data[consts.CHACHA_IV_SIZE + consts.POLY1305_MAC_SIZE :]
|
|
|
|
return crypto.chacha_poly_decrypt(
|
|
|
|
self.dek, key, iv, ciphertext + tag, key.to_bytes(2, sys.byteorder)
|
|
|
|
)
|
|
|
|
|
|
|
|
def _set_encrypt(self, key: int, val: bytes):
|
|
|
|
# In C, data are preallocated beforehand for encrypted values,
|
|
|
|
# to match the behaviour we do the same.
|
|
|
|
preallocate = b"\xFF" * (
|
|
|
|
consts.CHACHA_IV_SIZE + len(val) + consts.POLY1305_MAC_SIZE
|
|
|
|
)
|
|
|
|
self.nc.set(key, preallocate)
|
|
|
|
if consts.is_app_protected(key >> 8):
|
|
|
|
sat = self._calculate_authentication_tag()
|
|
|
|
self.nc.set(consts.SAT_KEY, sat)
|
|
|
|
|
|
|
|
iv = prng.random_buffer(consts.CHACHA_IV_SIZE)
|
|
|
|
cipher_text, tag = crypto.chacha_poly_encrypt(
|
|
|
|
self.dek, iv, val, key.to_bytes(2, sys.byteorder)
|
|
|
|
)
|
|
|
|
return self.nc.replace(key, iv + tag + cipher_text)
|
|
|
|
|
|
|
|
def _calculate_authentication_tag(self) -> bytes:
|
|
|
|
keys = []
|
|
|
|
for key in self.nc._get_all_keys():
|
|
|
|
if consts.is_app_protected(key >> 8):
|
|
|
|
keys.append(key.to_bytes(2, sys.byteorder))
|
|
|
|
if not keys:
|
|
|
|
return crypto.init_hmacs(self.sak)
|
|
|
|
return crypto.calculate_hmacs(self.sak, keys)
|
|
|
|
|
|
|
|
def _set_bool(self, key: int, val: bool) -> bool:
|
|
|
|
if val:
|
|
|
|
return self.nc.set(key, consts.TRUE_BYTE)
|
|
|
|
# False is stored as an empty value
|
|
|
|
return self.nc.set(key, consts.FALSE_BYTE)
|
|
|
|
|
|
|
|
def _dump(self) -> bytes:
|
|
|
|
return self.nc._dump()
|