canvasframework 0.3.14 → 0.3.15
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.
- package/components/CircularProgress.js +213 -29
- package/core/CanvasFramework.js +1 -0
- package/core/Component.js +90 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/utils/CryptoManager.js +303 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
* Spinner de chargement circulaire
|
|
4
|
+
* Spinner de chargement circulaire avec support Material et Cupertino
|
|
4
5
|
* @class
|
|
5
6
|
* @extends Component
|
|
6
7
|
* @property {number} size - Taille du spinner
|
|
@@ -21,8 +22,8 @@ class CircularProgress extends Component {
|
|
|
21
22
|
* @param {boolean} [options.indeterminate=true] - Mode indéterminé
|
|
22
23
|
* @param {number} [options.progress=0] - Progression (0-100)
|
|
23
24
|
* @param {string} [options.color] - Couleur (auto selon platform)
|
|
24
|
-
* @param {number} [options.lineWidth
|
|
25
|
-
* @param {number} [options.animationSpeed
|
|
25
|
+
* @param {number} [options.lineWidth] - Épaisseur (auto selon platform)
|
|
26
|
+
* @param {number} [options.animationSpeed] - Vitesse d'animation (auto selon platform)
|
|
26
27
|
*/
|
|
27
28
|
constructor(framework, options = {}) {
|
|
28
29
|
super(framework, options);
|
|
@@ -30,10 +31,32 @@ class CircularProgress extends Component {
|
|
|
30
31
|
this.indeterminate = options.indeterminate !== false;
|
|
31
32
|
this.progress = options.progress || 0; // 0-100
|
|
32
33
|
this.platform = framework.platform;
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
// Couleurs selon la plateforme
|
|
36
|
+
this.color = options.color || (
|
|
37
|
+
this.platform === 'material' ? '#6200EE' : '#8E8E93'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Épaisseur selon la plateforme
|
|
41
|
+
this.lineWidth = options.lineWidth || (
|
|
42
|
+
this.platform === 'material' ? 4 : 2.5
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Vitesse d'animation selon la plateforme
|
|
46
|
+
this.animationSpeed = options.animationSpeed || (
|
|
47
|
+
this.platform === 'material' ? 0.05 : 0.08
|
|
48
|
+
);
|
|
49
|
+
|
|
35
50
|
this.rotation = 0;
|
|
36
|
-
|
|
51
|
+
|
|
52
|
+
// Pour l'animation Material (arc qui s'agrandit/rétrécit)
|
|
53
|
+
this.arcStart = 0;
|
|
54
|
+
this.arcEnd = 0;
|
|
55
|
+
this.arcGrowing = true;
|
|
56
|
+
|
|
57
|
+
// Pour l'animation Cupertino (12 traits qui tournent)
|
|
58
|
+
this.cupertinoLines = 12;
|
|
59
|
+
this.cupertinoOpacity = Array(this.cupertinoLines).fill(0);
|
|
37
60
|
|
|
38
61
|
this.width = this.size;
|
|
39
62
|
this.height = this.size;
|
|
@@ -49,54 +72,164 @@ class CircularProgress extends Component {
|
|
|
49
72
|
* @private
|
|
50
73
|
*/
|
|
51
74
|
startAnimation() {
|
|
52
|
-
|
|
75
|
+
let lastTime = performance.now();
|
|
76
|
+
|
|
77
|
+
const animate = (currentTime) => {
|
|
53
78
|
if (!this.visible || !this.indeterminate) return;
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
const deltaTime = currentTime - lastTime;
|
|
81
|
+
lastTime = currentTime;
|
|
82
|
+
|
|
83
|
+
if (this.platform === 'material') {
|
|
84
|
+
this.animateMaterial(deltaTime);
|
|
85
|
+
} else {
|
|
86
|
+
this.animateCupertino(deltaTime);
|
|
58
87
|
}
|
|
59
88
|
|
|
89
|
+
this.markDirty();
|
|
60
90
|
requestAnimationFrame(animate);
|
|
61
91
|
};
|
|
62
|
-
|
|
92
|
+
|
|
93
|
+
requestAnimationFrame(animate);
|
|
63
94
|
}
|
|
64
95
|
|
|
65
96
|
/**
|
|
66
|
-
*
|
|
67
|
-
* @
|
|
97
|
+
* Animation Material (arc qui tourne et change de taille)
|
|
98
|
+
* @private
|
|
68
99
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
animateMaterial(deltaTime) {
|
|
101
|
+
// Rotation globale
|
|
102
|
+
this.rotation += this.animationSpeed;
|
|
103
|
+
if (this.rotation > Math.PI * 2) {
|
|
104
|
+
this.rotation -= Math.PI * 2;
|
|
105
|
+
}
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const radius = (this.size - this.lineWidth) / 2;
|
|
107
|
+
// Animation de l'arc (grossit puis rétrécit)
|
|
108
|
+
const arcSpeed = 0.03;
|
|
75
109
|
|
|
110
|
+
if (this.arcGrowing) {
|
|
111
|
+
this.arcEnd += arcSpeed;
|
|
112
|
+
if (this.arcEnd > Math.PI * 1.5) {
|
|
113
|
+
this.arcGrowing = false;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this.arcStart += arcSpeed;
|
|
117
|
+
if (this.arcStart >= this.arcEnd) {
|
|
118
|
+
this.arcGrowing = true;
|
|
119
|
+
this.arcStart = 0;
|
|
120
|
+
this.arcEnd = 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Animation Cupertino (traits qui s'estompent)
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
animateCupertino(deltaTime) {
|
|
130
|
+
this.rotation += this.animationSpeed;
|
|
131
|
+
if (this.rotation > Math.PI * 2) {
|
|
132
|
+
this.rotation -= Math.PI * 2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Calculer l'opacité de chaque trait (fade progressif)
|
|
136
|
+
const activeIndex = Math.floor((this.rotation / (Math.PI * 2)) * this.cupertinoLines);
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < this.cupertinoLines; i++) {
|
|
139
|
+
const distance = Math.abs(i - activeIndex);
|
|
140
|
+
const minDistance = Math.min(distance, this.cupertinoLines - distance);
|
|
141
|
+
this.cupertinoOpacity[i] = 1 - (minDistance / this.cupertinoLines) * 0.8;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Dessine le spinner Material
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
drawMaterial(ctx, centerX, centerY, radius) {
|
|
76
150
|
if (this.indeterminate) {
|
|
77
|
-
//
|
|
151
|
+
// Track (cercle de base - très léger)
|
|
152
|
+
ctx.strokeStyle = 'rgba(98, 0, 238, 0.1)';
|
|
153
|
+
ctx.lineWidth = this.lineWidth;
|
|
154
|
+
ctx.beginPath();
|
|
155
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
156
|
+
ctx.stroke();
|
|
157
|
+
|
|
158
|
+
// Arc animé qui tourne
|
|
159
|
+
ctx.save();
|
|
78
160
|
ctx.translate(centerX, centerY);
|
|
79
161
|
ctx.rotate(this.rotation);
|
|
80
|
-
ctx.translate(-centerX, -centerY);
|
|
81
162
|
|
|
82
|
-
|
|
83
|
-
ctx.strokeStyle = this.platform === 'material' ? '#E0E0E0' : '#E5E5EA';
|
|
84
|
-
ctx.lineWidth = this.lineWidth;
|
|
163
|
+
ctx.strokeStyle = this.color;
|
|
85
164
|
ctx.lineCap = 'round';
|
|
165
|
+
ctx.lineWidth = this.lineWidth;
|
|
166
|
+
ctx.beginPath();
|
|
167
|
+
ctx.arc(0, 0, radius, this.arcStart, this.arcEnd);
|
|
168
|
+
ctx.stroke();
|
|
169
|
+
|
|
170
|
+
ctx.restore();
|
|
171
|
+
} else {
|
|
172
|
+
// Progress circulaire déterminé
|
|
173
|
+
// Track
|
|
174
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
175
|
+
ctx.lineWidth = this.lineWidth;
|
|
86
176
|
ctx.beginPath();
|
|
87
177
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
88
178
|
ctx.stroke();
|
|
89
179
|
|
|
90
|
-
//
|
|
180
|
+
// Progress
|
|
181
|
+
const angle = (this.progress / 100) * Math.PI * 2;
|
|
91
182
|
ctx.strokeStyle = this.color;
|
|
183
|
+
ctx.lineCap = 'round';
|
|
92
184
|
ctx.beginPath();
|
|
93
|
-
ctx.arc(centerX, centerY, radius,
|
|
185
|
+
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
|
|
94
186
|
ctx.stroke();
|
|
95
187
|
|
|
188
|
+
// Pourcentage au centre
|
|
189
|
+
if (this.progress > 0) {
|
|
190
|
+
ctx.fillStyle = this.color;
|
|
191
|
+
ctx.font = `bold ${this.size / 3}px -apple-system, sans-serif`;
|
|
192
|
+
ctx.textAlign = 'center';
|
|
193
|
+
ctx.textBaseline = 'middle';
|
|
194
|
+
ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Dessine le spinner Cupertino (style iOS)
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
drawCupertino(ctx, centerX, centerY, radius) {
|
|
204
|
+
if (this.indeterminate) {
|
|
205
|
+
// Spinner iOS avec 12 traits
|
|
206
|
+
ctx.lineCap = 'round';
|
|
207
|
+
ctx.lineWidth = this.lineWidth;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < this.cupertinoLines; i++) {
|
|
210
|
+
const angle = (i / this.cupertinoLines) * Math.PI * 2;
|
|
211
|
+
const opacity = this.cupertinoOpacity[i];
|
|
212
|
+
|
|
213
|
+
ctx.save();
|
|
214
|
+
ctx.translate(centerX, centerY);
|
|
215
|
+
ctx.rotate(angle);
|
|
216
|
+
|
|
217
|
+
// Trait avec opacité variable
|
|
218
|
+
const startRadius = radius * 0.6;
|
|
219
|
+
const endRadius = radius;
|
|
220
|
+
|
|
221
|
+
ctx.strokeStyle = this.hexToRgba(this.color, opacity);
|
|
222
|
+
ctx.beginPath();
|
|
223
|
+
ctx.moveTo(0, -startRadius);
|
|
224
|
+
ctx.lineTo(0, -endRadius);
|
|
225
|
+
ctx.stroke();
|
|
226
|
+
|
|
227
|
+
ctx.restore();
|
|
228
|
+
}
|
|
96
229
|
} else {
|
|
97
|
-
// Progress circulaire
|
|
230
|
+
// Progress circulaire iOS (plus fin et élégant)
|
|
98
231
|
// Track
|
|
99
|
-
ctx.strokeStyle =
|
|
232
|
+
ctx.strokeStyle = '#E5E5EA';
|
|
100
233
|
ctx.lineWidth = this.lineWidth;
|
|
101
234
|
ctx.beginPath();
|
|
102
235
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
@@ -110,25 +243,76 @@ class CircularProgress extends Component {
|
|
|
110
243
|
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
|
|
111
244
|
ctx.stroke();
|
|
112
245
|
|
|
113
|
-
//
|
|
246
|
+
// Pas de texte au centre pour iOS (plus minimaliste)
|
|
247
|
+
// Mais si tu veux, décommente :
|
|
248
|
+
/*
|
|
114
249
|
if (this.progress > 0) {
|
|
115
250
|
ctx.fillStyle = this.color;
|
|
116
|
-
ctx.font =
|
|
251
|
+
ctx.font = `${this.size / 4}px -apple-system, sans-serif`;
|
|
117
252
|
ctx.textAlign = 'center';
|
|
118
253
|
ctx.textBaseline = 'middle';
|
|
119
254
|
ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
|
|
120
255
|
}
|
|
256
|
+
*/
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Dessine le spinner
|
|
262
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
263
|
+
*/
|
|
264
|
+
draw(ctx) {
|
|
265
|
+
ctx.save();
|
|
266
|
+
|
|
267
|
+
const centerX = this.x + this.size / 2;
|
|
268
|
+
const centerY = this.y + this.size / 2;
|
|
269
|
+
const radius = (this.size - this.lineWidth * 2) / 2;
|
|
270
|
+
|
|
271
|
+
if (this.platform === 'material') {
|
|
272
|
+
this.drawMaterial(ctx, centerX, centerY, radius);
|
|
273
|
+
} else {
|
|
274
|
+
this.drawCupertino(ctx, centerX, centerY, radius);
|
|
121
275
|
}
|
|
122
276
|
|
|
123
277
|
ctx.restore();
|
|
124
278
|
}
|
|
125
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Convertit une couleur hex en rgba avec opacité
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
hexToRgba(hex, alpha) {
|
|
285
|
+
// Si c'est déjà rgba, le retourner
|
|
286
|
+
if (hex.startsWith('rgba')) return hex;
|
|
287
|
+
if (hex.startsWith('rgb')) {
|
|
288
|
+
return hex.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Convertir hex en rgba
|
|
292
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
293
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
294
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
295
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
296
|
+
}
|
|
297
|
+
|
|
126
298
|
/**
|
|
127
299
|
* Définit la progression
|
|
128
300
|
* @param {number} value - Valeur de progression (0-100)
|
|
129
301
|
*/
|
|
130
302
|
setProgress(value) {
|
|
131
303
|
this.progress = Math.max(0, Math.min(100, value));
|
|
304
|
+
this.indeterminate = false;
|
|
305
|
+
this.markDirty();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Active le mode indéterminé
|
|
310
|
+
*/
|
|
311
|
+
setIndeterminate() {
|
|
312
|
+
this.indeterminate = true;
|
|
313
|
+
if (!this._animating) {
|
|
314
|
+
this.startAnimation();
|
|
315
|
+
}
|
|
132
316
|
}
|
|
133
317
|
|
|
134
318
|
/**
|
package/core/CanvasFramework.js
CHANGED
|
@@ -66,6 +66,7 @@ import FetchClient from '../utils/FetchClient.js';
|
|
|
66
66
|
import GeoLocationService from '../utils/GeoLocationService.js';
|
|
67
67
|
import WebSocketClient from '../utils/WebSocketClient.js';
|
|
68
68
|
import AnimationEngine from '../utils/AnimationEngine.js';
|
|
69
|
+
import CryptoManager from '../utils/CryptoManager.js';
|
|
69
70
|
|
|
70
71
|
// Features
|
|
71
72
|
import PullToRefresh from '../features/PullToRefresh.js';
|
package/core/Component.js
CHANGED
|
@@ -46,8 +46,98 @@ class Component {
|
|
|
46
46
|
|
|
47
47
|
// Pour détecter les updates
|
|
48
48
|
this._prevProps = { ...options };
|
|
49
|
+
|
|
50
|
+
// Système de listeners
|
|
51
|
+
this._listeners = new Map();
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Ajoute un listener pour un événement
|
|
56
|
+
* @param {string} event - Nom de l'événement
|
|
57
|
+
* @param {Function} handler - Fonction callback
|
|
58
|
+
* @returns {Component} - Pour le chaînage
|
|
59
|
+
*/
|
|
60
|
+
on(event, handler) {
|
|
61
|
+
if (!this._listeners.has(event)) {
|
|
62
|
+
this._listeners.set(event, []);
|
|
63
|
+
}
|
|
64
|
+
this._listeners.get(event).push(handler);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retire un listener
|
|
70
|
+
* @param {string} event - Nom de l'événement
|
|
71
|
+
* @param {Function} handler - Fonction à retirer
|
|
72
|
+
* @returns {Component}
|
|
73
|
+
*/
|
|
74
|
+
off(event, handler) {
|
|
75
|
+
if (!this._listeners.has(event)) return this;
|
|
76
|
+
|
|
77
|
+
const handlers = this._listeners.get(event);
|
|
78
|
+
const index = handlers.indexOf(handler);
|
|
79
|
+
if (index > -1) {
|
|
80
|
+
handlers.splice(index, 1);
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ajoute un listener qui s'exécute une seule fois
|
|
87
|
+
* @param {string} event - Nom de l'événement
|
|
88
|
+
* @param {Function} handler - Fonction callback
|
|
89
|
+
* @returns {Component}
|
|
90
|
+
*/
|
|
91
|
+
once(event, handler) {
|
|
92
|
+
const wrapper = (...args) => {
|
|
93
|
+
handler(...args);
|
|
94
|
+
this.off(event, wrapper);
|
|
95
|
+
};
|
|
96
|
+
return this.on(event, wrapper);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Émet un événement
|
|
101
|
+
* @param {string} event - Nom de l'événement
|
|
102
|
+
* @param {...any} args - Arguments à passer aux handlers
|
|
103
|
+
* @returns {Component}
|
|
104
|
+
*/
|
|
105
|
+
emit(event, ...args) {
|
|
106
|
+
if (!this._listeners.has(event)) return this;
|
|
107
|
+
|
|
108
|
+
const handlers = this._listeners.get(event);
|
|
109
|
+
for (let handler of handlers) {
|
|
110
|
+
try {
|
|
111
|
+
handler(...args);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Error in ${event} handler:`, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retire tous les listeners d'un événement (ou tous)
|
|
121
|
+
* @param {string} [event] - Nom de l'événement (optionnel)
|
|
122
|
+
* @returns {Component}
|
|
123
|
+
*/
|
|
124
|
+
removeAllListeners(event) {
|
|
125
|
+
if (event) {
|
|
126
|
+
this._listeners.delete(event);
|
|
127
|
+
} else {
|
|
128
|
+
this._listeners.clear();
|
|
129
|
+
}
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Retourne le nombre de listeners pour un événement
|
|
135
|
+
* @param {string} event - Nom de l'événement
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
listenerCount(event) {
|
|
139
|
+
return this._listeners.has(event) ? this._listeners.get(event).length : 0;
|
|
140
|
+
}
|
|
51
141
|
/* =======================
|
|
52
142
|
LIFECYCLE HOOKS
|
|
53
143
|
======================= */
|
package/index.js
CHANGED
|
@@ -74,6 +74,7 @@ export { default as FetchClient } from './utils/FetchClient.js';
|
|
|
74
74
|
export { default as GeoLocationService } from './utils/GeoLocationService.js';
|
|
75
75
|
export { default as WebSocketClient } from './utils/WebSocketClient.js';
|
|
76
76
|
export { default as AnimationEngine } from './utils/AnimationEngine.js';
|
|
77
|
+
export { default as CryptoManager } from './utils/CryptoManager.js';
|
|
77
78
|
|
|
78
79
|
// Features
|
|
79
80
|
export { default as PullToRefresh } from './features/PullToRefresh.js';
|
package/package.json
CHANGED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gestionnaire de cryptographie pour le Canvas Framework
|
|
3
|
+
* Utilise l'API Web Crypto (SubtleCrypto) pour un chiffrement sécurisé
|
|
4
|
+
* @class
|
|
5
|
+
*/
|
|
6
|
+
class CryptoManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Vérifier la disponibilité de l'API Crypto
|
|
9
|
+
if (!window.crypto || !window.crypto.subtle) {
|
|
10
|
+
throw new Error('Web Crypto API is not available in this browser');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this.crypto = window.crypto.subtle;
|
|
14
|
+
|
|
15
|
+
// Algorithme par défaut
|
|
16
|
+
this.algorithm = {
|
|
17
|
+
name: 'AES-GCM',
|
|
18
|
+
length: 256
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Stockage des clés en mémoire (pas dans localStorage !)
|
|
22
|
+
this._keys = new Map();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Génère une clé de chiffrement aléatoire
|
|
27
|
+
* @param {string} keyName - Nom de la clé pour référence ultérieure
|
|
28
|
+
* @returns {Promise<CryptoKey>}
|
|
29
|
+
*/
|
|
30
|
+
async generateKey(keyName = 'default') {
|
|
31
|
+
const key = await this.crypto.generateKey(
|
|
32
|
+
{
|
|
33
|
+
name: this.algorithm.name,
|
|
34
|
+
length: this.algorithm.length
|
|
35
|
+
},
|
|
36
|
+
true, // extractable
|
|
37
|
+
['encrypt', 'decrypt']
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this._keys.set(keyName, key);
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Importe une clé depuis une chaîne (base64)
|
|
46
|
+
* @param {string} keyString - Clé au format base64
|
|
47
|
+
* @param {string} keyName - Nom de la clé
|
|
48
|
+
* @returns {Promise<CryptoKey>}
|
|
49
|
+
*/
|
|
50
|
+
async importKey(keyString, keyName = 'default') {
|
|
51
|
+
const keyBuffer = this._base64ToBuffer(keyString);
|
|
52
|
+
|
|
53
|
+
const key = await this.crypto.importKey(
|
|
54
|
+
'raw',
|
|
55
|
+
keyBuffer,
|
|
56
|
+
{ name: this.algorithm.name },
|
|
57
|
+
true,
|
|
58
|
+
['encrypt', 'decrypt']
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
this._keys.set(keyName, key);
|
|
62
|
+
return key;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Exporte une clé au format base64
|
|
67
|
+
* @param {string} keyName - Nom de la clé
|
|
68
|
+
* @returns {Promise<string>}
|
|
69
|
+
*/
|
|
70
|
+
async exportKey(keyName = 'default') {
|
|
71
|
+
const key = this._keys.get(keyName);
|
|
72
|
+
if (!key) {
|
|
73
|
+
throw new Error(`Key "${keyName}" not found`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const exported = await this.crypto.exportKey('raw', key);
|
|
77
|
+
return this._bufferToBase64(exported);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Dérive une clé depuis un mot de passe
|
|
82
|
+
* @param {string} password - Mot de passe
|
|
83
|
+
* @param {string} keyName - Nom de la clé
|
|
84
|
+
* @param {string} salt - Salt (optionnel, généré automatiquement)
|
|
85
|
+
* @returns {Promise<{key: CryptoKey, salt: string}>}
|
|
86
|
+
*/
|
|
87
|
+
async deriveKeyFromPassword(password, keyName = 'default', salt = null) {
|
|
88
|
+
// Générer ou utiliser le salt fourni
|
|
89
|
+
const saltBuffer = salt
|
|
90
|
+
? this._base64ToBuffer(salt)
|
|
91
|
+
: window.crypto.getRandomValues(new Uint8Array(16));
|
|
92
|
+
|
|
93
|
+
// Encoder le mot de passe
|
|
94
|
+
const encoder = new TextEncoder();
|
|
95
|
+
const passwordBuffer = encoder.encode(password);
|
|
96
|
+
|
|
97
|
+
// Importer le mot de passe comme clé
|
|
98
|
+
const keyMaterial = await this.crypto.importKey(
|
|
99
|
+
'raw',
|
|
100
|
+
passwordBuffer,
|
|
101
|
+
{ name: 'PBKDF2' },
|
|
102
|
+
false,
|
|
103
|
+
['deriveBits', 'deriveKey']
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Dériver la clé
|
|
107
|
+
const key = await this.crypto.deriveKey(
|
|
108
|
+
{
|
|
109
|
+
name: 'PBKDF2',
|
|
110
|
+
salt: saltBuffer,
|
|
111
|
+
iterations: 100000,
|
|
112
|
+
hash: 'SHA-256'
|
|
113
|
+
},
|
|
114
|
+
keyMaterial,
|
|
115
|
+
{ name: this.algorithm.name, length: this.algorithm.length },
|
|
116
|
+
true,
|
|
117
|
+
['encrypt', 'decrypt']
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
this._keys.set(keyName, key);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
key,
|
|
124
|
+
salt: this._bufferToBase64(saltBuffer)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Chiffre des données
|
|
130
|
+
* @param {any} data - Données à chiffrer (sera converti en JSON)
|
|
131
|
+
* @param {string} keyName - Nom de la clé à utiliser
|
|
132
|
+
* @returns {Promise<{encrypted: string, iv: string}>}
|
|
133
|
+
*/
|
|
134
|
+
async encrypt(data, keyName = 'default') {
|
|
135
|
+
const key = this._keys.get(keyName);
|
|
136
|
+
if (!key) {
|
|
137
|
+
throw new Error(`Key "${keyName}" not found. Generate or import a key first.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Convertir les données en JSON puis en buffer
|
|
141
|
+
const dataString = typeof data === 'string' ? data : JSON.stringify(data);
|
|
142
|
+
const encoder = new TextEncoder();
|
|
143
|
+
const dataBuffer = encoder.encode(dataString);
|
|
144
|
+
|
|
145
|
+
// Générer un IV (Initialization Vector) aléatoire
|
|
146
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
147
|
+
|
|
148
|
+
// Chiffrer
|
|
149
|
+
const encryptedBuffer = await this.crypto.encrypt(
|
|
150
|
+
{
|
|
151
|
+
name: this.algorithm.name,
|
|
152
|
+
iv: iv
|
|
153
|
+
},
|
|
154
|
+
key,
|
|
155
|
+
dataBuffer
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
encrypted: this._bufferToBase64(encryptedBuffer),
|
|
160
|
+
iv: this._bufferToBase64(iv)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Déchiffre des données
|
|
166
|
+
* @param {string} encryptedData - Données chiffrées (base64)
|
|
167
|
+
* @param {string} iv - IV utilisé lors du chiffrement (base64)
|
|
168
|
+
* @param {string} keyName - Nom de la clé à utiliser
|
|
169
|
+
* @param {boolean} parseJson - Parser le résultat en JSON (true par défaut)
|
|
170
|
+
* @returns {Promise<any>}
|
|
171
|
+
*/
|
|
172
|
+
async decrypt(encryptedData, iv, keyName = 'default', parseJson = true) {
|
|
173
|
+
const key = this._keys.get(keyName);
|
|
174
|
+
if (!key) {
|
|
175
|
+
throw new Error(`Key "${keyName}" not found. Generate or import a key first.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Convertir les chaînes base64 en buffers
|
|
179
|
+
const encryptedBuffer = this._base64ToBuffer(encryptedData);
|
|
180
|
+
const ivBuffer = this._base64ToBuffer(iv);
|
|
181
|
+
|
|
182
|
+
// Déchiffrer
|
|
183
|
+
const decryptedBuffer = await this.crypto.decrypt(
|
|
184
|
+
{
|
|
185
|
+
name: this.algorithm.name,
|
|
186
|
+
iv: ivBuffer
|
|
187
|
+
},
|
|
188
|
+
key,
|
|
189
|
+
encryptedBuffer
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Convertir le buffer en string
|
|
193
|
+
const decoder = new TextDecoder();
|
|
194
|
+
const decryptedString = decoder.decode(decryptedBuffer);
|
|
195
|
+
|
|
196
|
+
// Parser en JSON si demandé
|
|
197
|
+
if (parseJson) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(decryptedString);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Si le parsing échoue, retourner la chaîne brute
|
|
202
|
+
return decryptedString;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return decryptedString;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Chiffre et encode en une seule chaîne (pratique pour stockage)
|
|
211
|
+
* @param {any} data - Données à chiffrer
|
|
212
|
+
* @param {string} keyName - Nom de la clé
|
|
213
|
+
* @returns {Promise<string>} - Chaîne contenant données chiffrées + IV
|
|
214
|
+
*/
|
|
215
|
+
async encryptToString(data, keyName = 'default') {
|
|
216
|
+
const { encrypted, iv } = await this.encrypt(data, keyName);
|
|
217
|
+
return `${encrypted}.${iv}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Déchiffre depuis une chaîne créée par encryptToString
|
|
222
|
+
* @param {string} encryptedString - Chaîne chiffrée
|
|
223
|
+
* @param {string} keyName - Nom de la clé
|
|
224
|
+
* @param {boolean} parseJson - Parser en JSON
|
|
225
|
+
* @returns {Promise<any>}
|
|
226
|
+
*/
|
|
227
|
+
async decryptFromString(encryptedString, keyName = 'default', parseJson = true) {
|
|
228
|
+
const [encrypted, iv] = encryptedString.split('.');
|
|
229
|
+
if (!encrypted || !iv) {
|
|
230
|
+
throw new Error('Invalid encrypted string format');
|
|
231
|
+
}
|
|
232
|
+
return this.decrypt(encrypted, iv, keyName, parseJson);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Hash une chaîne (non réversible)
|
|
237
|
+
* @param {string} data - Données à hasher
|
|
238
|
+
* @param {string} algorithm - Algorithme (SHA-256, SHA-384, SHA-512)
|
|
239
|
+
* @returns {Promise<string>} - Hash en base64
|
|
240
|
+
*/
|
|
241
|
+
async hash(data, algorithm = 'SHA-256') {
|
|
242
|
+
const encoder = new TextEncoder();
|
|
243
|
+
const dataBuffer = encoder.encode(data);
|
|
244
|
+
|
|
245
|
+
const hashBuffer = await this.crypto.digest(algorithm, dataBuffer);
|
|
246
|
+
return this._bufferToBase64(hashBuffer);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Génère un token aléatoire sécurisé
|
|
251
|
+
* @param {number} length - Longueur en octets (32 par défaut)
|
|
252
|
+
* @returns {string} - Token en base64
|
|
253
|
+
*/
|
|
254
|
+
generateToken(length = 32) {
|
|
255
|
+
const buffer = window.crypto.getRandomValues(new Uint8Array(length));
|
|
256
|
+
return this._bufferToBase64(buffer);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Supprime une clé de la mémoire
|
|
261
|
+
* @param {string} keyName - Nom de la clé
|
|
262
|
+
*/
|
|
263
|
+
deleteKey(keyName) {
|
|
264
|
+
this._keys.delete(keyName);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Supprime toutes les clés
|
|
269
|
+
*/
|
|
270
|
+
deleteAllKeys() {
|
|
271
|
+
this._keys.clear();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Liste les clés disponibles
|
|
276
|
+
* @returns {string[]}
|
|
277
|
+
*/
|
|
278
|
+
listKeys() {
|
|
279
|
+
return Array.from(this._keys.keys());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ===== Méthodes utilitaires =====
|
|
283
|
+
|
|
284
|
+
_bufferToBase64(buffer) {
|
|
285
|
+
const bytes = new Uint8Array(buffer);
|
|
286
|
+
let binary = '';
|
|
287
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
288
|
+
binary += String.fromCharCode(bytes[i]);
|
|
289
|
+
}
|
|
290
|
+
return btoa(binary);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_base64ToBuffer(base64) {
|
|
294
|
+
const binary = atob(base64);
|
|
295
|
+
const bytes = new Uint8Array(binary.length);
|
|
296
|
+
for (let i = 0; i < binary.length; i++) {
|
|
297
|
+
bytes[i] = binary.charCodeAt(i);
|
|
298
|
+
}
|
|
299
|
+
return bytes.buffer;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default CryptoManager;
|