cryptologin 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ """
2
+ CryptoLogin - Zero-Knowledge Authentication System
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "CryptoLogin Team"
7
+ __license__ = "Apache-2.0"
8
+
9
+ from .core.crypto_engine import CryptoEngine
10
+ from .core.user_manager import UserManager
11
+ from .core.data_vault import DataVault
12
+ from .core.exceptions import (
13
+ CryptoLoginError,
14
+ CryptoError,
15
+ InvalidSecretError,
16
+ DecryptionError,
17
+ IntegrityError,
18
+ UserNotFoundError,
19
+ UserAlreadyExistsError,
20
+ AuthenticationError,
21
+ DataVaultError,
22
+ )
23
+ from .storage.sqlite import SQLiteStorage
24
+ from .storage.memory import MemoryStorage
25
+ from .config import get_settings, Settings
26
+ from .main import app
27
+
28
+
29
+ class CryptoLogin:
30
+ """
31
+ Main entry point for CryptoLogin.
32
+
33
+ Example:
34
+ >>> from cryptologin import CryptoLogin
35
+ >>> auth = CryptoLogin()
36
+ >>> user_id = auth.register("my-secret", {"name": "John"})
37
+ """
38
+
39
+ def __init__(self, db_path: str = "cryptologin.db"):
40
+ from .core.user_manager import UserManager
41
+ from .storage.sqlite import SQLiteStorage
42
+ from .core.crypto_engine import CryptoEngine
43
+
44
+ self.storage = SQLiteStorage(db_path=db_path)
45
+ self.crypto_engine = CryptoEngine()
46
+ self.user_manager = UserManager(storage=self.storage)
47
+
48
+ def register(self, master_secret: str, user_data: dict = None) -> str:
49
+ """Register a new user."""
50
+ return self.user_manager.register_user(master_secret, user_data)
51
+
52
+ def login_init(self, master_secret: str) -> str:
53
+ """Initiate login - returns challenge."""
54
+ return self.user_manager.initiate_login(master_secret)
55
+
56
+ def login_verify(self, master_secret: str, challenge_response: str):
57
+ """Complete login - returns session."""
58
+ return self.user_manager.complete_login(master_secret, challenge_response)
59
+
60
+ def get_user_data(self, user_id: str, master_secret: str) -> dict:
61
+ """Get user data."""
62
+ return self.user_manager.get_user_data(user_id, master_secret)
63
+
64
+ def update_user_data(self, user_id: str, master_secret: str, data: dict) -> bool:
65
+ """Update user data."""
66
+ return self.user_manager.update_user_data(user_id, master_secret, data)
67
+
68
+ def delete_user(self, user_id: str, master_secret: str) -> bool:
69
+ """Delete a user."""
70
+ from .core.exceptions import AuthenticationError
71
+ # Verify secret matches
72
+ derived_id = self.crypto_engine.derive_user_id(master_secret)
73
+ if derived_id != user_id:
74
+ raise AuthenticationError("Secret does not match user")
75
+ return self.user_manager.delete_user(user_id)
76
+
77
+
78
+ # Export cli main for entry point
79
+ from . import cli
80
+
81
+
82
+ __all__ = [
83
+ "CryptoLogin",
84
+ "CryptoEngine",
85
+ "UserManager",
86
+ "DataVault",
87
+ "SQLiteStorage",
88
+ "MemoryStorage",
89
+ "CryptoLoginError",
90
+ "CryptoError",
91
+ "InvalidSecretError",
92
+ "DecryptionError",
93
+ "IntegrityError",
94
+ "UserNotFoundError",
95
+ "UserAlreadyExistsError",
96
+ "AuthenticationError",
97
+ "DataVaultError",
98
+ "get_settings",
99
+ "Settings",
100
+ "app",
101
+ "__version__",
102
+ "cli",
103
+ ]
@@ -0,0 +1,7 @@
1
+ # TODO: Implement logic
2
+
3
+ def main():
4
+ pass
5
+
6
+ if __name__ == '__main__':
7
+ main()
@@ -0,0 +1,113 @@
1
+ """
2
+ Dépendances pour l'API CryptoLogin
3
+ """
4
+ from typing import Optional
5
+ from fastapi import Request, HTTPException, status, Depends
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from fastapi.responses import JSONResponse
8
+
9
+ from ..core.user_manager import UserManager
10
+ from ..storage.sqlite import SQLiteStorage
11
+ from ..config import get_settings, Settings
12
+ from slowapi import Limiter
13
+ from slowapi.util import get_remote_address
14
+ from slowapi.errors import RateLimitExceeded
15
+
16
+ # Singleton pour UserManager
17
+ _user_manager_instance = None
18
+ _storage_instance = None
19
+
20
+
21
+ # ============================================================
22
+ # STORAGE - Singleton
23
+ # ============================================================
24
+
25
+ def get_storage(settings: Settings = Depends(get_settings)) -> SQLiteStorage:
26
+ """Retourne l'instance de stockage (Singleton)."""
27
+ global _storage_instance
28
+ if _storage_instance is None:
29
+ db_path = settings.DATABASE_URL.replace("sqlite:///", "")
30
+ _storage_instance = SQLiteStorage(db_path=db_path, auto_migrate=True)
31
+ return _storage_instance
32
+
33
+
34
+ # ============================================================
35
+ # USER MANAGER - Singleton
36
+ # ============================================================
37
+
38
+ def get_user_manager(storage: SQLiteStorage = Depends(get_storage)) -> UserManager:
39
+ """Retourne l'instance du UserManager (Singleton)."""
40
+ global _user_manager_instance
41
+ if _user_manager_instance is None:
42
+ _user_manager_instance = UserManager(storage=storage, session_duration_hours=24)
43
+ return _user_manager_instance
44
+
45
+
46
+ # ============================================================
47
+ # AUTHENTIFICATION
48
+ # ============================================================
49
+
50
+ security = HTTPBearer(auto_error=False)
51
+
52
+
53
+ async def get_current_user(
54
+ request: Request,
55
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
56
+ user_manager: UserManager = Depends(get_user_manager)
57
+ ) -> str:
58
+ """
59
+ Valide la session et retourne l'ID utilisateur.
60
+ """
61
+ if not credentials:
62
+ raise HTTPException(
63
+ status_code=status.HTTP_401_UNAUTHORIZED,
64
+ detail="Not authenticated",
65
+ headers={"WWW-Authenticate": "Bearer"},
66
+ )
67
+
68
+ user_id = credentials.credentials
69
+
70
+ # Vérifier la session
71
+ if not user_manager.validate_session(user_id):
72
+ raise HTTPException(
73
+ status_code=status.HTTP_401_UNAUTHORIZED,
74
+ detail="Invalid or expired session",
75
+ headers={"WWW-Authenticate": "Bearer"},
76
+ )
77
+
78
+ return user_id
79
+
80
+
81
+ # ============================================================
82
+ # RATE LIMITING
83
+ # ============================================================
84
+
85
+ limiter = Limiter(key_func=get_remote_address)
86
+
87
+
88
+ def get_limiter() -> Limiter:
89
+ """Retourne l'instance du rate limiter."""
90
+ return limiter
91
+
92
+
93
+ def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
94
+ """
95
+ Gestionnaire personnalisé pour les erreurs de rate limiting.
96
+ """
97
+ return JSONResponse(
98
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
99
+ content={
100
+ "error": "Rate limit exceeded",
101
+ "detail": f"Too many requests. Please wait {exc.retry_after} seconds.",
102
+ "status_code": status.HTTP_429_TOO_MANY_REQUESTS,
103
+ "retry_after": exc.retry_after
104
+ }
105
+ )
106
+
107
+
108
+ # Réinitialiser les singletons pour les tests
109
+ def reset_singletons():
110
+ """Réinitialise les singletons (pour les tests)."""
111
+ global _user_manager_instance, _storage_instance
112
+ _user_manager_instance = None
113
+ _storage_instance = None
@@ -0,0 +1,133 @@
1
+ """
2
+ Modèles Pydantic pour l'API CryptoLogin - Version Pydantic v2
3
+ """
4
+ from pydantic import BaseModel, Field, field_validator
5
+ from typing import Optional, Dict, Any
6
+ from datetime import datetime
7
+
8
+ # ============================================================
9
+ # REQUESTS
10
+ # ============================================================
11
+
12
+ class RegisterRequest(BaseModel):
13
+ """Requête d'enregistrement."""
14
+ master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
15
+ user_data: Optional[Dict[str, Any]] = Field(default=None, description="Données initiales de l'utilisateur")
16
+
17
+ @field_validator('master_secret')
18
+ @classmethod
19
+ def validate_secret(cls, v: str) -> str:
20
+ if len(v) < 32:
21
+ raise ValueError('Master secret must be at least 32 characters')
22
+ return v
23
+
24
+
25
+ class LoginInitRequest(BaseModel):
26
+ """Requête d'initiation de login."""
27
+ master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
28
+
29
+ @field_validator('master_secret')
30
+ @classmethod
31
+ def validate_secret(cls, v: str) -> str:
32
+ if len(v) < 32:
33
+ raise ValueError('Master secret must be at least 32 characters')
34
+ return v
35
+
36
+
37
+ class LoginVerifyRequest(BaseModel):
38
+ """Requête de vérification de login."""
39
+ master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
40
+ challenge_response: str = Field(..., description="Réponse au challenge (déchiffré)")
41
+
42
+ @field_validator('master_secret')
43
+ @classmethod
44
+ def validate_secret(cls, v: str) -> str:
45
+ if len(v) < 32:
46
+ raise ValueError('Master secret must be at least 32 characters')
47
+ return v
48
+
49
+
50
+ class UpdateDataRequest(BaseModel):
51
+ """Requête de mise à jour des données."""
52
+ master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
53
+ data: Dict[str, Any] = Field(..., description="Nouvelles données")
54
+
55
+ @field_validator('master_secret')
56
+ @classmethod
57
+ def validate_secret(cls, v: str) -> str:
58
+ if len(v) < 32:
59
+ raise ValueError('Master secret must be at least 32 characters')
60
+ return v
61
+
62
+
63
+ class RotateSecretRequest(BaseModel):
64
+ """Requête de rotation de secret."""
65
+ old_secret: str = Field(..., min_length=32, description="Ancien secret")
66
+ new_secret: str = Field(..., min_length=32, description="Nouveau secret")
67
+
68
+ @field_validator('old_secret', 'new_secret')
69
+ @classmethod
70
+ def validate_secret(cls, v: str) -> str:
71
+ if len(v) < 32:
72
+ raise ValueError('Secret must be at least 32 characters')
73
+ return v
74
+
75
+
76
+ class DeleteUserRequest(BaseModel):
77
+ """Requête de suppression d'utilisateur."""
78
+ master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
79
+
80
+ @field_validator('master_secret')
81
+ @classmethod
82
+ def validate_secret(cls, v: str) -> str:
83
+ if len(v) < 32:
84
+ raise ValueError('Master secret must be at least 32 characters')
85
+ return v
86
+
87
+ # ============================================================
88
+ # RESPONSES
89
+ # ============================================================
90
+
91
+
92
+ class UserResponse(BaseModel):
93
+ """Réponse utilisateur."""
94
+ user_id: str
95
+ created_at: datetime
96
+ updated_at: datetime
97
+ last_activity_at: Optional[datetime]
98
+ has_data: bool
99
+ has_vault: bool
100
+
101
+
102
+ class DataResponse(BaseModel):
103
+ """Réponse des données."""
104
+ data: Dict[str, Any]
105
+ version: str = "1.0"
106
+
107
+
108
+ class AuthInitResponse(BaseModel):
109
+ """Réponse d'initiation de login."""
110
+ challenge: str
111
+ message: str = "Please decrypt the challenge and submit it with /verify"
112
+
113
+
114
+ class AuthResponse(BaseModel):
115
+ """Réponse d'authentification."""
116
+ user_id: str
117
+ session_id: str
118
+ expires_at: datetime
119
+ message: str = "Authentication successful"
120
+
121
+
122
+ class MessageResponse(BaseModel):
123
+ """Réponse de message simple."""
124
+ message: str
125
+ success: bool = True
126
+ data: Optional[Dict[str, Any]] = None
127
+
128
+
129
+ class ErrorResponse(BaseModel):
130
+ """Réponse d'erreur."""
131
+ error: str
132
+ detail: Optional[str] = None
133
+ status_code: int
@@ -0,0 +1,6 @@
1
+ """
2
+ Package des routes API
3
+ """
4
+ from . import auth, user, health
5
+
6
+ __all__ = ['auth', 'user', 'health']
@@ -0,0 +1,198 @@
1
+ """
2
+ Routes d'authentification
3
+ """
4
+ from typing import Any
5
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from ...core.user_manager import UserManager
9
+ from ...core.exceptions import (
10
+ UserNotFoundError,
11
+ UserAlreadyExistsError,
12
+ AuthenticationError,
13
+ InvalidSecretError
14
+ )
15
+ from ..models import (
16
+ RegisterRequest,
17
+ LoginInitRequest,
18
+ LoginVerifyRequest,
19
+ AuthInitResponse,
20
+ AuthResponse,
21
+ MessageResponse,
22
+ ErrorResponse
23
+ )
24
+ from ..dependencies import get_user_manager, get_current_user
25
+ from ..dependencies import limiter as rate_limiter
26
+
27
+ router = APIRouter(prefix="/auth", tags=["Authentication"])
28
+
29
+
30
+ @router.post(
31
+ "/register",
32
+ response_model=MessageResponse,
33
+ responses={
34
+ 400: {"model": ErrorResponse},
35
+ 409: {"model": ErrorResponse},
36
+ 500: {"model": ErrorResponse}
37
+ }
38
+ )
39
+ @rate_limiter.limit("10/minute")
40
+ async def register(
41
+ request: Request, # Ajouté pour rate_limiter
42
+ user_data: RegisterRequest,
43
+ user_manager: UserManager = Depends(get_user_manager)
44
+ ) -> Any:
45
+ """Enregistre un nouvel utilisateur."""
46
+ try:
47
+ user_id = user_manager.register_user(
48
+ user_data.master_secret,
49
+ user_data.user_data or {}
50
+ )
51
+
52
+ return MessageResponse(
53
+ message="User registered successfully",
54
+ data={"user_id": user_id}
55
+ )
56
+
57
+ except InvalidSecretError as e:
58
+ raise HTTPException(
59
+ status_code=status.HTTP_400_BAD_REQUEST,
60
+ detail=str(e)
61
+ )
62
+ except UserAlreadyExistsError as e:
63
+ raise HTTPException(
64
+ status_code=status.HTTP_409_CONFLICT,
65
+ detail=str(e)
66
+ )
67
+ except Exception as e:
68
+ raise HTTPException(
69
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
70
+ detail=f"Registration failed: {str(e)}"
71
+ )
72
+
73
+
74
+ @router.post(
75
+ "/login/init",
76
+ response_model=AuthInitResponse,
77
+ responses={
78
+ 400: {"model": ErrorResponse},
79
+ 401: {"model": ErrorResponse},
80
+ 404: {"model": ErrorResponse},
81
+ 500: {"model": ErrorResponse}
82
+ }
83
+ )
84
+ @rate_limiter.limit("30/minute")
85
+ async def login_init(
86
+ request: Request, # Ajouté pour rate_limiter
87
+ login_data: LoginInitRequest,
88
+ user_manager: UserManager = Depends(get_user_manager)
89
+ ) -> Any:
90
+ """Initie le processus de login."""
91
+ try:
92
+ challenge = user_manager.initiate_login(login_data.master_secret)
93
+
94
+ return AuthInitResponse(
95
+ challenge=challenge,
96
+ message="Please decrypt the challenge and submit it with /verify"
97
+ )
98
+
99
+ except InvalidSecretError as e:
100
+ raise HTTPException(
101
+ status_code=status.HTTP_400_BAD_REQUEST,
102
+ detail=str(e)
103
+ )
104
+ except UserNotFoundError as e:
105
+ raise HTTPException(
106
+ status_code=status.HTTP_404_NOT_FOUND,
107
+ detail=str(e)
108
+ )
109
+ except AuthenticationError as e:
110
+ raise HTTPException(
111
+ status_code=status.HTTP_401_UNAUTHORIZED,
112
+ detail=str(e)
113
+ )
114
+ except Exception as e:
115
+ raise HTTPException(
116
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
117
+ detail=f"Login initiation failed: {str(e)}"
118
+ )
119
+
120
+
121
+ @router.post(
122
+ "/login/verify",
123
+ response_model=AuthResponse,
124
+ responses={
125
+ 400: {"model": ErrorResponse},
126
+ 401: {"model": ErrorResponse},
127
+ 404: {"model": ErrorResponse},
128
+ 500: {"model": ErrorResponse}
129
+ }
130
+ )
131
+ @rate_limiter.limit("30/minute")
132
+ async def login_verify(
133
+ request: Request, # Ajouté pour rate_limiter
134
+ verify_data: LoginVerifyRequest,
135
+ user_manager: UserManager = Depends(get_user_manager)
136
+ ) -> Any:
137
+ """Vérifie la réponse au challenge et crée une session."""
138
+ try:
139
+ session = user_manager.complete_login(
140
+ verify_data.master_secret,
141
+ verify_data.challenge_response
142
+ )
143
+
144
+ return AuthResponse(
145
+ user_id=session.user_id,
146
+ session_id=session.user_id,
147
+ expires_at=session.expires_at,
148
+ message="Authentication successful"
149
+ )
150
+
151
+ except InvalidSecretError as e:
152
+ raise HTTPException(
153
+ status_code=status.HTTP_400_BAD_REQUEST,
154
+ detail=str(e)
155
+ )
156
+ except UserNotFoundError as e:
157
+ raise HTTPException(
158
+ status_code=status.HTTP_404_NOT_FOUND,
159
+ detail=str(e)
160
+ )
161
+ except AuthenticationError as e:
162
+ raise HTTPException(
163
+ status_code=status.HTTP_401_UNAUTHORIZED,
164
+ detail=str(e)
165
+ )
166
+ except Exception as e:
167
+ raise HTTPException(
168
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
169
+ detail=f"Login verification failed: {str(e)}"
170
+ )
171
+
172
+
173
+ @router.post(
174
+ "/logout",
175
+ response_model=MessageResponse,
176
+ responses={
177
+ 401: {"model": ErrorResponse},
178
+ 500: {"model": ErrorResponse}
179
+ }
180
+ )
181
+ @rate_limiter.limit("60/minute")
182
+ async def logout(
183
+ request: Request, # Ajouté pour rate_limiter
184
+ user_id: str = Depends(get_current_user),
185
+ user_manager: UserManager = Depends(get_user_manager)
186
+ ) -> Any:
187
+ """Déconnecte l'utilisateur."""
188
+ try:
189
+ user_manager.logout(user_id)
190
+ return MessageResponse(
191
+ message="Logged out successfully"
192
+ )
193
+
194
+ except Exception as e:
195
+ raise HTTPException(
196
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
197
+ detail=f"Logout failed: {str(e)}"
198
+ )
@@ -0,0 +1,59 @@
1
+ """
2
+ Routes de santé - Pas de rate limiting nécessaire
3
+ """
4
+ from typing import Any
5
+ from fastapi import APIRouter, Depends, status
6
+ from pydantic import BaseModel
7
+ from datetime import datetime
8
+
9
+ from ...config import get_settings, Settings
10
+ from ...storage.sqlite import SQLiteStorage
11
+ from ..dependencies import get_storage
12
+
13
+ router = APIRouter(prefix="/health", tags=["Health"])
14
+
15
+
16
+ class HealthResponse(BaseModel):
17
+ """Réponse de santé."""
18
+ status: str
19
+ version: str
20
+ timestamp: str
21
+ database: str
22
+ uptime: str
23
+
24
+
25
+ class ReadyResponse(BaseModel):
26
+ """Réponse de disponibilité."""
27
+ ready: bool
28
+ database: bool
29
+
30
+
31
+ @router.get("", response_model=HealthResponse)
32
+ async def health_check(
33
+ settings: Settings = Depends(get_settings)
34
+ ) -> Any:
35
+ """Vérifie l'état de santé de l'application."""
36
+ return HealthResponse(
37
+ status="healthy",
38
+ version=settings.APP_VERSION,
39
+ timestamp=datetime.now().isoformat(),
40
+ database="sqlite",
41
+ uptime="running"
42
+ )
43
+
44
+
45
+ @router.get("/ready", response_model=ReadyResponse)
46
+ async def ready_check(
47
+ storage: SQLiteStorage = Depends(get_storage)
48
+ ) -> Any:
49
+ """Vérifie si l'application est prête à servir."""
50
+ try:
51
+ storage.get_user_count()
52
+ db_ready = True
53
+ except Exception:
54
+ db_ready = False
55
+
56
+ return ReadyResponse(
57
+ ready=db_ready,
58
+ database=db_ready
59
+ )