Dans la plupart des établissements, les Tableaux Blancs Interactifs (TBI) sont devenus le cœur des cours numériques : ils affichent des ressources en ligne, des applications pédagogiques ou des outils de collaboration. Pourtant, dès qu’il s’agit de maintenir une session authentifiée — sans imposer aux étudiants de saisir un mot de passe à chaque cours et sans risquer la fuite de ces identifiants dans les DevTools ou le gestionnaire du navigateur — le défi devient concret :
- Comment démarrer automatiquement une application Web protégée sur un TBI, sans jamais exposer le mot de passe ?
- Comment verrouiller l’interface en mode kiosque tout en gardant une porte de sortie (fermeture propre) ?
- Comment packager cette solution en un exécutable autonome, facile à déployer et à maintenir ?
- Comment protéger votre code JavaScript embarqué (app.asar) pour éviter que quelqu’un n’en extraie les credentials en clair ?
Ce guide pas à pas vous montre une architecture robuste basé sur le projet electron de github pour répondre à ces besoins :
- Authentification automatique via un preload sécurisé,
- Mode kiosque adaptable entre développement et production,
- Chiffrement de votre configuration (login/mot de passe) en AES-256,
- Packaging et signature de l’application pour un déploiement clé en main.
Table of Contents
📋 Prérequis
- Environnement de développement
- Node.js v16+ et npm
- Git (optionnel)
- Outils
- Visual Studio Code (ou votre IDE préféré)
- Powershell (Windows) ou Terminal (Unix)
- Scoop / Chocolatey / Homebrew pour installer les Build Tools (Windows/macOS)
- Certificat de signature (optionnel mais recommandé pour
safeStorage
) - Connaissances de base en JavaScript et ligne de commande
Création du projet Electron
- Initialiser le dossier
mkdir secure-electron-login && cd secure-electron-login
npm init -y
- Installer Electron et le builder
npm install electron --save-dev
npm install electron-builder cross-env --save-dev
- Structure initiale
secure-electron-login/
├── main.js
├── preload.js
├── package.json
└── config.json
Fichiers de configuration pour le dev
Créer config.json
à la racine :
{
« login »: « professeur@tbi.ex »,
« password »: « MotDePasseUltraSecret »
}
Créer encrypt-config.js
pour générer le .enc
:
// encrypt-config.js
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ⚠️ même clé que STATIC_ENCRYPTION_KEY dans main.js
const KEY = Buffer.from(
'4f62b5c8d9e0a1b2c3d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b',
'hex'
);
const inPath = path.join(__dirname, 'config.json');
const outPath = path.join(__dirname, 'config.enc');
// lit le JSON en clair
const data = fs.readFileSync(inPath);
// génère un IV aléatoire (16 octets)
const iv = crypto.randomBytes(16);
// chiffre avec AES-256-CBC
const cipher = crypto.createCipheriv('aes-256-cbc', KEY, iv);
const encrypted = Buffer.concat([iv, cipher.update(data), cipher.final()]);
// écrit le buffer [iv|payload]
fs.writeFileSync(outPath, encrypted);
console.log('✅ config.enc généré');
Contenu de main.js
Import et clés
// main.js
const { app, BrowserWindow, ipcMain, Menu } = require('electron');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
// Clé AES-256 (32 octets hex)
const STATIC_KEY = Buffer.from(
'4f62b5c8d9e0a1b2c3d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b', 'hex'
);
Fonction de chargement des credentials
const isDev = process.env.NODE_ENV === 'development';
function loadCredentials() {
if (isDev) {
// DEV : on lit config.json à la racine
const jsonPath = path.join(__dirname, 'config.json');
if (!fs.existsSync(jsonPath)) throw new Error('config.json introuvable en dev');
return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
}
// PROD : on lit config.enc dans le dossier des ressources
const encPath = path.join(process.resourcesPath, 'config.enc');
if (!fs.existsSync(encPath)) throw new Error('config.enc introuvable en prod');
const buf = fs.readFileSync(encPath);
const iv = buf.slice(0, 16), data = buf.slice(16);
const decipher = crypto.createDecipheriv('aes-256-cbc', STATIC_KEY, iv);
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
return JSON.parse(decrypted.toString());
}
Création de la fenêtre et injection
let credentials;
function createWindow() {
const win = new BrowserWindow({
width: 1280, height: 800,
kiosk: false,
fullscreen: !isDev,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
win.loadURL('https://monapp.ex');
win.webContents.on('did-finish-load', () => {
win.webContents.send('credentials', credentials);
});
if (isDev) {
win.webContents.openDevTools({ mode: 'detach' });
Menu.setApplicationMenu(Menu.buildFromTemplate([
{ label: 'Dev', submenu: [{ role: 'reload' }, { role: 'toggleDevTools' }] }
]));
} else {
Menu.setApplicationMenu(Menu.buildFromTemplate([
{ label: 'Fichier', submenu: [{ role: 'quit', label: 'Quitter' }] }
]));
}
win.webContents.on('before-input-event', (e, input) => {
if (isDev && input.control && input.shift && input.key.toLowerCase() === 'i') {
win.webContents.toggleDevTools();
e.preventDefault();
}
});
}
app.whenReady()
.then(() => { credentials = loadCredentials(); })
.then(createWindow)
.catch(err => {
console.error('Erreur à l’initialisation :', err);
app.quit();
});
Contenu de preload.js
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
window.addEventListener('DOMContentLoaded', () => {
const style = document.createElement('style');
style.textContent = `
/* Cache et désactive l'icône œil et tout élément lié */
#show_password,
.show-password,
.password-indicator {
display: none !important;
pointer-events: none !important;
}
`;
document.head.appendChild(style);
});
contextBridge.exposeInMainWorld('secureLogin', {
// facultatif si tu veux déclencher manuellement
fillLoginForm: () => ipcRenderer.send('fill-login')
});
ipcRenderer.on('credentials', (_, { login, password }) => {
const tryInject = () => {
const loginInput = document.querySelector('#user_email');
const passwordInput = document.querySelector('#password_field');
const form = passwordInput?.closest('form');
if (loginInput && passwordInput && form) {
loginInput.value = login;
passwordInput.value = password;
form.submit();
} else {
// réessaie toutes les 200 ms si les éléments ne sont pas encore présents
setTimeout(tryInject, 200);
}
};
tryInject();
});
Configuration de package.json
{
"name": "secure-electron-login",
"version": "1.0.0",
"description": "Application kiosque Electron pour Visible Body avec login automatique sécurisé",
"author": "Pascal Mietlicki",
"main": "main.js",
"scripts": {
"dev": "cross-env NODE_ENV=development electron .",
"encrypt-config": "node encrypt-config.js",
"dist": "npm run encrypt-config && electron-builder",
"start": "npm run dist && start \"\" \"dist\\win-unpacked\\VisibleBodySecureLogin.exe\""
},
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "^29.0.0",
"electron-builder": "^26.0.12"
},
"build": {
"extraResources": [
{ "from": "config.enc", "to": "config.enc" }
],
"appId": "com.visiblebody.securelogin",
"productName": "VisibleBodySecureLogin",
"files": [
"main.js",
"preload.js",
"package.json"
],
"directories": {
"output": "dist"
},
"win": {
"target": [
"nsis",
"portable",
"zip"
],
"icon": "assets/icon.ico"
},
"mac": {
"target": "dmg",
"icon": "assets/icon.icns"
},
"linux": {
"target": "AppImage",
"icon": "assets"
}
}
}
Signature et safeStorage
(optionnel)
Windows (.pfx
) :
"build": {
"win": {
"certificateFile": "moncert.pfx",
"certificatePassword": "monMotDePasseCertif"
}
}
macOS (Developer ID) :
"build": {
"mac": {
"identity": "Developer ID Application: MonOrg (TEAMID)"
}
}
Activation de safeStorage :
const { safeStorage } = require('electron');
if (app.isPackaged && safeStorage.isEncryptionAvailable()) {
// safeStorage.encryptString / decryptString utilisables
}
VII. Commandes résumé rapide
- Chiffrement de la configuration (
config.json
→config.enc
)npm run encrypt-config
- Dév :
npm install
npm run dev
- Build/Prod :
npm install
npm run start
(Génèreconfig.enc
, compile, signe, lance l’exécutable.)
npm run start
exécute en chaîneencrypt-config
,dist
et lance l’EXE généré.Pour macOS/Linux, adaptez la commande
start
du scriptpackage.json
afin d’ouvrir directement votre binaire.