Hi everyone,
This is a really interesting thread about simple ways to save TiddlyWiki! I especially appreciate the tip from @AlfieA and the discussion about file naming and browser behaviors.
While the topic is focused on simple saving, the conversation got me thinking about security, and I wanted to introduce another layer of protection that users might find valuable, especially if their TiddlyWiki contains sensitive information.
We’ve been discussing ways to easily save and reopen TiddlyWiki files. But what about protecting access to that information itself? The proposed mechanism with time based filenames provides a good way to create backups of your TiddlyWiki, but it does not protect against unauthorized access to its content. That’s where the Time-Based One-Time Password (TOTP) and libsodium encryption come in.
The TOTP/Libsodium Idea:
The concept is to combine TOTP authentication with libsodium
to encrypt and secure your TiddlyWiki from unauthorized access. Here’s a brief summary of how it could work:
-
Initial Setup:
- The user sets up TOTP using a key generated by a TOTP application or other TOTP provider.
- The password you chose to protect your TiddlyWiki will be used as a key in order to decrypt the TOTP Key.
-
Encryption: The TOTP secret key is encrypted using a key generated from the password.
-
Saving the Encrypted Key: The encrypted key is stored within the TiddlyWiki file.
-
Access:
- When the user opens the TiddlyWiki, a login form will be displayed.
- The user will be prompted for the password they chose during setup. This password will be used to decrypt the TOTP key.
- The user must also provide the current TOTP code generated by an authenticator app.
- If both the password and the code are valid, the TiddlyWiki is unlocked.
Why Use TOTP/Libsodium in This Context?
-
Increased Security: It adds an extra layer of security against unauthorized access to the TiddlyWiki content, even if someone gets their hands on the HTML file. The time-based backup files could be stored without much risk, as the content is protected by TOTP.
-
Flexibility: TOTP works on any device with a TOTP app, so it doesn’t necessarily lock you into a single environment. The combination of a password and TOTP allows for two-factor authentication.
-
Privacy: With the encryption, the content of your TiddlyWiki is protected.
A Conceptual Approach (Based on Previous Discussion)
I’ve been exploring a possible implementation in the form of a plugin. It’s not a production-ready solution, but here’s a simplified version of the core ideas using JavaScript in a TiddlyWiki plugin:
(function() {
/*jshint esversion: 6 */
exports.name = 'totp-auth';
exports.version = '0.1.0';
const sodium = require('libsodium-wrappers'); // or tweetnacl
function deriveKey(password, salt) {
const passwordBuffer = new TextEncoder().encode(password);
const saltBuffer = new Uint8Array(salt);
return sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
passwordBuffer,
saltBuffer,
sodium.crypto_pwhash_OPSLIMIT_MODERATE,
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
sodium.crypto_pwhash_ALG_DEFAULT
);
}
exports.startup = function() {
const storage = $tw.wiki.getTiddlerData("$:/plugins/YourPluginName/storage",{});
// Function to generate the libsodium key pair and encrypted TOTP key
$tw.wiki.generateEncryptedTotpKey = function (password, totpKey) {
const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
const derivedKey = deriveKey(password, salt);
const keyPair = sodium.crypto_box_keypair();
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const totpKeyBuffer = new TextEncoder().encode(totpKey);
const encryptedTotpKey = sodium.crypto_box(totpKeyBuffer, nonce, keyPair.publicKey, keyPair.privateKey);
// Save all, the salt must be stored in plain text because it is used to decrypt the password
storage.sodiumPublicKey = sodium.to_base64(keyPair.publicKey);
storage.encryptedTotpKey = sodium.to_base64(encryptedTotpKey);
storage.nonce = sodium.to_base64(nonce);
storage.salt = sodium.to_base64(salt);
$tw.wiki.setTiddlerData("$:/plugins/YourPluginName/storage", storage);
return true;
};
// Function to decrypt the TOTP key and generate the code
$tw.wiki.getTotpCode = function (password) {
if(!storage.salt || !storage.encryptedTotpKey || !storage.nonce || !storage.sodiumPublicKey) {
return undefined;
}
const salt = sodium.from_base64(storage.salt);
const derivedKey = deriveKey(password, salt);
const encryptedTotpKey = sodium.from_base64(storage.encryptedTotpKey);
const nonce = sodium.from_base64(storage.nonce);
const publicKey = sodium.from_base64(storage.sodiumPublicKey);
const privateKey = sodium.crypto_box_keypair(publicKey, derivedKey);
try {
const decryptedTotpKeyBuffer = sodium.crypto_box_open(encryptedTotpKey, nonce, publicKey, privateKey);
const decryptedTotpKey = new TextDecoder().decode(decryptedTotpKeyBuffer);
const totp = require('jsotp');
const totpInstance = totp.TOTP(decryptedTotpKey);
return totpInstance.now();
} catch (e) {
//console.error(e)
return undefined;
}
};
$tw.wiki.isAuthenticated = function() {
return $tw.wiki.getTiddlerText("$:/state/totp/authenticated") === "true";
};
$tw.wiki.updateAuthenticatedState = function (isAuthenticated) {
$tw.wiki.setText("$:/state/totp/authenticated", undefined, isAuthenticated ? "true" : "false");
}
};
})();
<div id="login-form" style="display:none">
<h2>Login TOTP</h2>
<p>
Mot de passe:
<input type="password" id="password-input">
</p>
<p>
Code TOTP:
<input type="text" id="totp-input">
</p>
<p>
<button id="login-button">Valider TOTP</button>
</p>
<p>
<button id="generate-key-button">Générer clé TOTP</button>
</p>
<div id="error-message" style="color:red"></div>
</div>
<script>
const storage = $tw.wiki.getTiddlerData("$:/plugins/YourPluginName/storage",{});
if(!$tw.wiki.isAuthenticated())
{
const loginDiv = document.getElementById('login-form');
loginDiv.style.display = 'block';
const passwordInput = document.getElementById('password-input');
const totpInput = document.getElementById('totp-input');
const errorMessage = document.getElementById('error-message');
if (!storage.sodiumPublicKey){
document.getElementById("login-button").style.display = 'none';
document.getElementById('generate-key-button').addEventListener('click', function() {
const password = passwordInput.value;
const totpKey = prompt("Entrez votre clé TOTP (ex: 12345678901234567890)");
if(totpKey && password){
if ($tw.wiki.generateEncryptedTotpKey(password, totpKey)){
errorMessage.textContent = "Clé TOTP sauvegardée. Vous pouvez vous authentifier.";
document.getElementById("login-button").style.display = 'inline-block';
document.getElementById("generate-key-button").style.display = 'none';
} else {
errorMessage.textContent = "Erreur lors de la sauvegarde de la clé TOTP.";
}
} else {
errorMessage.textContent = "Veuillez fournir un mot de passe et une clé TOTP.";
}
});
}else{
document.getElementById("generate-key-button").style.display = 'none';
document.getElementById('login-button').addEventListener('click', function() {
const password = passwordInput.value;
const totpCode = totpInput.value;
if(!password || !totpCode){
errorMessage.textContent = "Veuillez entrer un mot de passe et un code TOTP.";
return;
}
const generatedCode = $tw.wiki.getTotpCode(password);
if(generatedCode && generatedCode === totpCode){
errorMessage.textContent = "Authentification réussie.";
$tw.wiki.updateAuthenticatedState(true)
loginDiv.style.display = 'none';
} else {
errorMessage.textContent = "Code TOTP invalide ou mot de passe incorrect.";
}
});
}
}
</script>
Important Notes:
-
This is a conceptual idea: This is not a production-ready plugin. Further work, testing, and security review are needed.
-
Libraries: This code would need the
libsodium-wrappers
or tweetnacl
and jsotp
libraries to be loaded in TiddlyWiki
Discussion:
While the current focus is on simpler save methods, I thought this idea might be interesting for users looking for enhanced security.
I’m curious to hear your thoughts on this approach. Do you think this kind of security is worth exploring? Any suggestions or feedback are welcome!