def test_encode_non_utf8_payload(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode(b"", timestamp=0) assert token == "4sfD0vPFhIif8cy4nB3BQkHeJqkOkDvinI4zIhMjYX4YXZU5WIq9ycCVjGzB5"
def other_impl_decode(string: bytes): with mock.branca_impl(): with mock.with_config() as config: key = config["manabi"]["key"] f = Branca(from_string(key)) ct = f.encode(string) proc = run(["cargo", "run", "decode", key, ct], stdout=PIPE, check=True) assert from_string(proc.stdout.decode("UTF-8")) == string
def test_encode_eight_nul_bytes_with_zero_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode(b"\x00\x00\x00\x00\x00\x00\x00\x00", timestamp=0) assert token == "1jIBheHbDdkCDFQmtgw4RUZeQoOJgGwTFJSpwOAk3XYpJJr52DEpILLmmwYl4tjdSbbNqcF1"
def test_encode_hello_world_with_november_27_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode("Hello world!", timestamp=123206400) assert token == "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT"
def test_encode_hello_world_with_max_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode("Hello world!", timestamp=4294967295) assert token == "89i7YCwu5tWAJNHUDdmIqhzOi5hVHOd4afjZcGMcVmM4enl4yeLiDyYv41eMkNmTX6IwYEFErCSqr"
def test_encode_hello_world_with_zero_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode("Hello world!", timestamp=0) assert token == "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z"
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_encode_eight_nul_bytes_with_november_27_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode(b"\x00\x00\x00\x00\x00\x00\x00\x00", timestamp=123206400) assert token == "1jJDJOEjuwVb9Csz1Ypw1KBWSkr0YDpeBeJN6NzJWx1VgPLmcBhu2SbkpQ9JjZ3nfUf7Aytp"
def test_encode_eight_nul_bytes_with_zero_timestamp(): key = unhexlify( "73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974") branca = Branca(key) branca._nonce = unhexlify( "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef") token = branca.encode(b"\x00\x00\x00\x00\x00\x00\x00\x00", timestamp=4294967295) assert token == "1jrx6DUu5q06oxykef2e2ZMyTcDRTQot9ZnwgifUtzAphGtjsxfbxXNhQyBEOGtpbkBgvIQx"
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
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
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
def _encode(key: bytes, path: str, now: Optional[int] = None) -> str: f = Branca(key) p = path.encode("UTF-8") ciphertext = f.encode(p, now) return ciphertext