def test_should_throw_with_wrong_key(): key = unhexlify( "77726f6e677365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_with_modified_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "870g1RCk4lW1YInhaU3TP8u2hGtfol16ettLcTOSoA0JIpjCaQRW7tQeP6dQmTvFIB2s6wL5deMXr" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_with_modified_nonce(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "875GH233SUysT7fQ711EWd9BXpwOjB72ng3ZLnjWFrmOqVy49Bv93b78JU5331LbcY0EEzhLfpmSx" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_with_modified_version(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "89mvl3S0BE0UCMIY94xxIux4eg1w5oXrhvCEXrDAjusSbO0Yk7AU6FjjTnbTWTqogLfNPJLzecHVb" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_with_wrong_version(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "89mvl3RkwXjpEj5WMxK7GUDEHEeeeZtwjMIOogTthvr44qBfYtQSIZH5MHOTC0GzoutDIeoPVZk3w" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_with_modified_tag(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trk0" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_with_modified_ciphertext(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5Qw6Jpo96myliI3hHD7VbKZBYh" with pytest.raises(RuntimeError): branca.decode(token)
def test_should_throw_when_expired(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "0102030405060708090a0b0c0102030405060708090a0b0c") token = branca.encode(b"Hello world!", timestamp=123206400) with pytest.raises(RuntimeError): branca.decode(token, 3600)
def test_decode_hello_world_with_zero_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z" assert branca.decode(token) == b"Hello world!" assert branca.timestamp(token) == 0
def test_decode_hello_world_with_max_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "89i7YCwu5tWAJNHUDdmIqhzOi5hVHOd4afjZcGMcVmM4enl4yeLiDyYv41eMkNmTX6IwYEFErCSqr" assert branca.decode(token) == b"Hello world!" assert branca.timestamp(token) == 4294967295
def test_should_allow_hex_string_key(): branca = Branca( key="73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") token = "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT" assert branca.decode(token) == b"Hello world!" assert branca.timestamp(token) == 123206400
def test_decode_eight_nul_bytes_with_nov27_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "1jJDJOEjuwVb9Csz1Ypw1KBWSkr0YDpeBeJN6NzJWx1VgPLmcBhu2SbkpQ9JjZ3nfUf7Aytp" assert branca.decode(token) == b"\x00\x00\x00\x00\x00\x00\x00\x00" assert branca.timestamp(token) == 123206400
def test_decode_eight_nul_bytes_with_max_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "1jrx6DUu5q06oxykef2e2ZMyTcDRTQot9ZnwgifUtzAphGtjsxfbxXNhQyBEOGtpbkBgvIQx" assert branca.decode(token) == b"\x00\x00\x00\x00\x00\x00\x00\x00" assert branca.timestamp(token) == 4294967295
def test_decode_eight_nul_bytes_with_zero_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "1jIBheHbDdkCDFQmtgw4RUZeQoOJgGwTFJSpwOAk3XYpJJr52DEpILLmmwYl4tjdSbbNqcF1" assert branca.decode(token) == b"\x00\x00\x00\x00\x00\x00\x00\x00" assert branca.timestamp(token) == 0
def test_decode_hello_world_with_nov27_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT" assert branca.decode(token) == b"Hello world!" assert branca.timestamp(token) == 123206400
def test_decode_empty_payload(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "4sfD0vPFhIif8cy4nB3BQkHeJqkOkDvinI4zIhMjYX4YXZU5WIq9ycCVjGzB5" assert branca.decode(token) == b"" assert branca.timestamp(token) == 0
def test_decode_non_utf8_payload(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) token = "K9u6d0zjXp8RXNUGDyXAsB9AtPo60CD3xxQ2ulL8aQoTzXbvockRff0y1eXoHm" assert branca.decode(token) == b"\x80" assert branca.timestamp(token) == 123206400
def from_ciphertext(cls, key: Key, ciphertext: str) -> "Token": assert ciphertext branca = Branca(key.data) try: timestamp = branca.timestamp(ciphertext) except (struct.error, ValueError): return cls(key, None, None) try: token_path = Path(branca.decode(ciphertext).decode("UTF-8")) except RuntimeError: return cls(key, None, timestamp) return cls(key, token_path, timestamp, ciphertext)
def test_branca_roundtrip(string: bytes): with mock.with_config() as config: key = config["manabi"]["key"] f = Branca(from_string(key)) res = f.decode(f.encode(string)) assert res == string
class ApiTokens: """A class for managing branca-based API tokens.""" def __init__(self, app, secret): """Initialize the Api Token manager. Parameters ---------- app : aiohttp.web.Application The aiohttp application instance secret : str The secret key used to sign the API tokens (after hashing) """ self.app = app self.branca = Branca(key=sha3_256(secret.encode("utf-8")).digest()) logger.debug("created Branca instance") async def generate_token(self, username, token_name=str(uuid4()), permissions="*"): """Generate a token for a user with given permissions. Parameters ---------- username : str The username of the user for whom the token is being generated token_name : str, default=str(uuid4()) The name of the token to be generated - will represent it on the frontend permissions : str, default="*" The permissions that the token will have acces to. "*" means all permissions user has Returns ------- (token, token_name, permissions) : tuple(str, str, list(str)) The generated token, its name and permissions it has access to """ if not token_name: token_name = str(uuid4()) logger.debug( "generating a token for %s with these permissions: %s", username, permissions, ) user = await find_user_by_username(self.app, username, ["permissions", "password", "_id"]) if permissions == "*": permissions = list(user.get("permissions", [])) else: permissions = list( filter( lambda permission: permission in user.get( "permissions", []), permissions, )) packed = msgpack.dumps({ "username": username, "permisisons": permissions, "token_name": token_name }) token = self.branca.encode(packed) logger.debug( "successfuly generated a token for %s: %s. Adding it to the database", username, token, ) await add_token_to_user(self.app, user.get("_id", ""), token_name, token) return ( token, token_name, permissions, ) async def validate_token(self, token, permissions=None, raise_error=False): """Validate if token is valid and has the requested permissions. Parameters ---------- token : str The token to be validated permissions : list(str), default=None The permissions that the token must have access to. raise_error : bool, default=False If True, raise an error if the token is invalid, otherwise just return False Returns ------- bool True if the token is valid and has the requested permissions, False otherwise """ permissions = permissions if permissions else [] logger.debug( "validating a token%s. Token: %s", " for permissions" + permissions if permissions != [] else "", token, ) user = await find_user_by_token(self.app, token, ["permissions", "username"]) packed = self.branca.decode(token) payload = msgpack.loads(packed, raw=False) logger.debug("decoded token: %s", payload) if user is None: if raise_error: raise HTTPUnauthorized() return False user_permission_set = set(user.get("permissions", [])) if not set(payload.get("permissions", [])).issubset(user_permission_set): payload["permissions"] = filter( lambda permission: permission in user_permission_set, payload["permissions"], ) if ( # check if token has all requested permissions not set(permissions).issubset( set(payload.get("permissions", []))) # check if user has all requested permissions or not set(permissions).issubset(user_permission_set)): if raise_error: raise HTTPForbidden() return False logger.debug("Token is valid") return True async def get_token_info(self, token): """Get information about a given token. Includes permissions, username, token name, etc. Parameters ---------- token : str The token to be checked Returns ------- (payload, user) : tuple(dict, dict) A dictionary containing information about the token and a dict with user information (username, user permissions) """ user = await find_user_by_token(self.app, token, ["permissions", "username"]) packed = self.branca.decode(token) payload = msgpack.loads(packed, raw=False) if user is None: return payload, None user_permission_set = set(user.get("permissions", [])) if not set(payload.get("permissions", [])).issubset(user_permission_set): payload["permissions"] = filter( lambda permission: permission in user_permission_set, payload["permissions"], ) return payload, user
class ApiTokens: def __init__(self, app, secret): self.app = app self.branca = Branca(key=sha3_256(secret.encode("utf-8")).digest()) logger.debug("created Branca instance") async def generate_token(self, username, token_name=str(uuid4()), permissions="*"): if not token_name: token_name = str(uuid4()) logger.debug( f"generating a token for {username} with these permissions: {permissions}" ) user = await find_user_by_username( self.app, username, ["permissions", "password", "_id"] ) if permissions == "*": permissions = list(user.get("permissions", [])) else: permissions = list( filter( lambda permission: permission in user.get("permissions", []), permissions, ) ) packed = msgpack.dumps( {"username": username, "permisisons": permissions, "token_name": token_name} ) token = self.branca.encode(packed) logger.debug( f"successfuly generated a token for {username}: {token}. Adding it to the database" ) await add_token_to_user(self.app, user.get("_id", ""), token_name, token) return ( token, token_name, permissions, ) async def validate_token(self, token, permissions=[], raise_error=False): logger.debug( f"validating a token{' for permissions' + permissions if permissions != [] else ''}. Token: {token}" ) user = await find_user_by_token(self.app, token, ["permissions", "username"]) packed = self.branca.decode(token) payload = msgpack.loads(packed, raw=False) logger.debug(f"decoded token: {payload}") if user == None: if raise_error: raise HTTPUnauthorized() return False user_permission_set = set(user.get("permissions", [])) if not set(payload.get("permissions", [])).issubset(user_permission_set): payload["permissions"] = filter( lambda permission: permission in user_permission_set, payload["permissions"], ) if ( # check if token has all requested permissions not set(permissions).issubset(set(payload.get("permissions", []))) # check if user has all requested permissions or not set(permissions).issubset(user_permission_set) ): if raise_error: raise HTTPForbidden() return False logger.debug("Token is valid") return True async def get_token_info(self, token): user = await find_user_by_token(self.app, token, ["permissions", "username"]) packed = self.branca.decode(token) payload = msgpack.loads(packed, raw=False) if user == None: return payload, None user_permission_set = set(user.get("permissions", [])) if not set(payload.get("permissions", [])).issubset(user_permission_set): payload["permissions"] = filter( lambda permission: permission in user_permission_set, payload["permissions"], ) return payload, user
def _decode(key: bytes, ciphertext: str, ttl=None) -> str: f = Branca(key) return f.decode(ciphertext, ttl).decode("UTF-8")