"""
Minimal pure-Python library that implements a basic version of a
secure decentralized biometric authentication functionality via
a secure multi-party computation protocol.
"""
from __future__ import annotations
from typing import Dict, List, Tuple, Sequence, Iterable
import doctest
import math
from modulo import modulo
import tinynmc
_PRECISION = 8
"""
Precision (*i.e.*, number of digits after the decimal point in a binary
representation of a value) for fixed-point rationals.
"""
def _encode(
descriptor: Sequence[float],
for_auth: bool = False
) -> Dict[Tuple[int, int], int]:
"""
Encode data for requests to nodes.
"""
reg_0_or_auth_1 = int(for_auth)
encoding = [round(value * (2 ** _PRECISION)) for value in descriptor]
coords_to_values = {
(reg_0_or_auth_1, 0): \
sum(value ** 2 for value in encoding)
}
for (index, value) in enumerate(encoding, 2):
coords_to_values[(index, reg_0_or_auth_1)] = (-2 if for_auth else 1) * value
return coords_to_values
[docs]class node(tinynmc.node):
"""
Data structure for maintaining the information associated with a node
and performing node operations.
Suppose that a workflows is supported by three nodes (parties performing
the decentralized registration and authentication functions). The :obj:`node`
objects would be instantiated locally by each of these three parties.
>>> nodes = [node(), node(), node()]
The preprocessing phase that the nodes must execute can be simulated using
the :obj:`preprocess` function. It is assumed that biometric descriptors
used for registration and authentication are represented as lists of
:obj:`float` values. All such descriptors must be of the same length,
and this length must be supplied as the second argument to the
:obj:`preprocess` function.
>>> preprocess(nodes, length=4)
It is then possible for a client to register itself by obtaining
a registration :obj:`token`. Suppose the client has a biometric
descriptor represented as a vector of floating point values. The
client can create a :obj:`request` for masks using the
:obj:`request.registration` method.
>>> reg_descriptor = [0.5, 0.3, 0.7, 0.1]
>>> reg_request = request.registration(reg_descriptor)
The client can deliver the request to each node, at which point that node
can locally use its :obj:`~tinynmc.tinynmc.node.masks` method (inherited
from the :obj:`tinynmc.tinynmc.node` class) to generate masks that can
be returned to the requesting client.
>>> reg_masks = [node.masks(reg_request) for node in nodes]
The client can then generate locally a registration :obj:`token` (*i.e.*, a
masked descriptor) via the :obj:`token.registration` method.
>>> reg_token = token.registration(reg_masks, reg_descriptor)
At any later point, it is possible to perform an authentication workflow.
Masks for the authentication descriptor can be requested via a process
that parallels the one for registration (in this case using the
:obj:`request.authentication` method).
>>> auth_descriptor = [0.1, 0.4, 0.8, 0.2]
>>> auth_request = request.authentication(auth_descriptor)
>>> auth_masks = [node.masks(auth_request) for node in nodes]
Given the masks for the authentication descriptor, the authentication
:obj:`token` (*i.e.*, a masked descriptor) can be generated locally by
the client via the :obj:`token.authentication` method.
>>> auth_token = token.authentication(auth_masks, auth_descriptor)
Finally, the client can broadcast its original registration token together
with its authentication token. Each node can then compute locally its share of
the authentication result. These shares can be reconstructed by the validating
party using the :obj:`reveal` function to obtain the Euclidean distance
between the registration and authentication descriptors.
>>> shares = [node.authenticate(reg_token, auth_token) for node in nodes]
>>> result = reveal(shares)
The tests below confirm that the computed result is indeed the Euclidean
distance.
>>> abs(result - 0.43) <= 0.05 # Use comparison for floating point value.
True
>>> abs(result - math.sqrt(sum(
... (x - y) ** 2
... for (x, y) in zip(reg_descriptor, auth_descriptor)
... ))) <= 0.05
True
"""
[docs] def authenticate(
self: node,
registration_token: token,
authentication_token: token
) -> modulo:
"""
Perform computation associated with an authentication workflow.
:param registration_token: Registration token to be used in the
local computation by this node.
:param authentication_token: Authentication token to be used in the
local computation by this node.
"""
return self.compute(
getattr(self, '_signature'),
[registration_token, authentication_token]
)
[docs]class request(List[Tuple[int, int]]):
"""
Data structure for representing registration and authentication requests.
"""
[docs] @staticmethod
def registration(descriptor: Sequence[float]) -> request:
"""
Encode descriptor into a registration request.
:param descriptor: Biometric descriptor to be used for registration.
This request can be submitted to each node to obtain masks for the
descriptor.
>>> reg_descriptor = [0.5, 0.3, 0.7, 0.1]
>>> isinstance(request.registration(reg_descriptor), request)
True
"""
return request(_encode(descriptor, False).keys())
[docs] @staticmethod
def authentication(descriptor: Sequence[float]) -> request:
"""
Encode descriptor into an authentication request.
:param descriptor: Biometric descriptor to be used for authentication.
This request can be submitted to each node to obtain masks for the
descriptor.
>>> auth_descriptor = [0.1, 0.4, 0.8, 0.2]
>>> isinstance(request.authentication(auth_descriptor), request)
True
"""
return request(_encode(descriptor, True).keys())
[docs]class token(Dict[Tuple[int, int], modulo]):
"""
Data structure for representing registration and authentication tokens.
"""
[docs] @staticmethod
def registration(
masks: Iterable[Dict[Tuple[int, int], modulo]],
descriptor: Sequence[float]
) -> token:
"""
Mask descriptor and create a registration token.
:param masks: Collection of masks to be applied to the descriptor.
:param descriptor: Biometric descriptor to be converted into a token.
Suppose masks have already been obtained from the nodes via the steps
below.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, 4)
>>> descriptor = [0.5, 0.3, 0.7, 0.1]
>>> masks = [node.masks(request.registration(descriptor)) for node in nodes]
This method can be used to mask the original descriptor (in preparation
for broadcasting it to the nodes).
>>> isinstance(token.registration(masks, descriptor), token)
True
"""
return token(tinynmc.masked_factors(_encode(descriptor, False), masks))
[docs] @staticmethod
def authentication(
masks: Iterable[Dict[Tuple[int, int], modulo]],
descriptor: Sequence[float]
) -> token:
"""
Mask descriptor and create an authentication token.
:param masks: Collection of masks to be applied to the descriptor.
:param descriptor: Biometric descriptor to be converted into a token.
Suppose masks have already been obtained from the nodes via the steps
below.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, 4)
>>> descriptor = [0.5, 0.3, 0.7, 0.1]
>>> masks = [node.masks(request.authentication(descriptor)) for node in nodes]
This method can be used to mask the original descriptor (in preparation
for broadcasting it to the nodes).
>>> isinstance(token.authentication(masks, descriptor), token)
True
"""
return token(tinynmc.masked_factors(_encode(descriptor, True), masks))
[docs]def preprocess(nodes: Sequence[node], length: int):
"""
Simulate a preprocessing phase among the collection of nodes for a workflow
that supports registration and authentication descriptor vectors of the
specified length.
:param nodes: Collection of nodes involved in the workflow.
:param length: Number of components in each descriptor list to be used
in the workflow.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, length=4)
"""
signature = [1, 1] + ([2] * length)
tinynmc.preprocess(signature, nodes)
for node_ in nodes:
setattr(node_, '_signature', signature)
[docs]def reveal(shares: Iterable[modulo]) -> float:
"""
Reconstruct the result of the overall workflow from its shares and convert
it into a meaningful output (*i.e.*, the Euclidean distance between the
registration descriptor and the authentication descriptor).
:param shares: Shares that can be reconstructed into a result.
Suppose the shares below are returned from the three nodes in a workflow.
>>> p = 4215209819
>>> shares = [modulo(2042458237, p), modulo(1046840547, p), modulo(1125923365, p)]
This method converts a collection of secret shares from the nodes into
a floating point value representing the Euclidean distance between the
registration and authentication descriptors.
>>> abs(reveal(shares) - 0.43) <= 0.05 # Use comparison for floating point value.
True
"""
return math.sqrt(int(sum(shares)) / (2 ** (2 * _PRECISION)))
if __name__ == '__main__':
doctest.testmod() # pragma: no cover