Skip to content

Commit 0871267

Browse files
Refactor authentication and IMAP configuration: switch to base64-encoded storage for master password hash and IMAP config
Signed-off-by: Shahm Najeeb <Shahm_Najeeb@outlook.com>
1 parent d1347e9 commit 0871267

8 files changed

Lines changed: 91 additions & 39 deletions

File tree

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ MASTER_PASSWORD_BCRYPT_HASH=your-bcrypt-hash-here
6565
# Generate with: openssl rand -base64 32
6666
SESSION_SECRET=your-random-secret-here
6767

68-
# IMAP Configuration (JSON format)
69-
# Add all your email accounts in this JSON structure
70-
IMAP_CONFIG={"accounts":[{"id":"account1","label":"Account 1","imap":{"host":"imap.example.com","port":993,"secure":true,"user":"user@example.com","password":"your-password"}}]}
68+
# IMAP Configuration (base64-encoded JSON)
69+
# Create your JSON config, then encode: Buffer.from(JSON.stringify(config)).toString('base64')
70+
IMAP_CONFIG=base64-encoded-json-here
7171
```
7272

7373
### 2. IMAP Configuration Details
7474

75-
> VERY IMPORTANT: Please escape all $ signs in passwords with a backslash (`\$`) when setting environment variables
75+
The `IMAP_CONFIG` environment variable accepts a base64-encoded JSON string with the following structure:
7676

77-
The `IMAP_CONFIG` environment variable accepts a JSON object with the following structure:
77+
**First, create your JSON configuration:**
7878

7979
```json
8080
{
@@ -94,6 +94,16 @@ The `IMAP_CONFIG` environment variable accepts a JSON object with the following
9494
}
9595
```
9696

97+
**Then encode it to base64:**
98+
99+
```javascript
100+
const config = {
101+
"accounts": [...]
102+
};
103+
const base64Config = Buffer.from(JSON.stringify(config)).toString('base64');
104+
console.log(base64Config);
105+
```
106+
97107
**Example with multiple accounts:**
98108

99109
```json
@@ -150,8 +160,8 @@ The `IMAP_CONFIG` environment variable accepts a JSON object with the following
150160
- User: Your full email address
151161
- Password: Your email password or app-specific password
152162

153-
**Important**: The IMAP_CONFIG value must be a single-line JSON string with no line breaks when set as an environment
154-
variable.
163+
**Note**: With base64 encoding, special characters in passwords (like `$`, `#`, `!`) are automatically handled - no
164+
escaping needed!
155165

156166
See [SECURITY.md](./SECURITY.md) for detailed configuration instructions.
157167

SECURITY.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,32 @@ It provides **read-only** access to IMAP mailboxes through a secure web interfac
5757
### Required Variables
5858

5959
```bash
60-
# Master password hash (generate with bcryptjs)
60+
# Master password hash (base64-encoded bcrypt hash)
6161
MASTER_PASSWORD_BCRYPT_HASH=
6262

6363
# Session secret (generate a random string)
6464
SESSION_SECRET=
6565

66-
# IMAP configuration (JSON format)
66+
# IMAP configuration (base64-encoded JSON)
6767
IMAP_CONFIG=
6868
```
6969

7070
### Generating MASTER_PASSWORD_HASH
7171

72+
The master password hash is stored as a base64-encoded bcrypt hash:
73+
7274
```javascript
7375
import bcrypt from 'bcryptjs'
7476

7577
const hash = await bcrypt.hash('your-secure-password', 10)
76-
console.log(hash)
78+
const base64Hash = Buffer.from(hash).toString('base64')
79+
console.log(base64Hash)
80+
```
81+
82+
Or use the provided setup script:
83+
84+
```bash
85+
node scripts/setup-password.js
7786
```
7887

7988
### Generating SESSION_SECRET
@@ -84,6 +93,8 @@ openssl rand -base64 32
8493

8594
### IMAP Configuration Format
8695

96+
The IMAP configuration is stored as base64-encoded JSON. First, create your JSON configuration:
97+
8798
```json
8899
{
89100
"accounts": [
@@ -102,6 +113,18 @@ openssl rand -base64 32
102113
}
103114
```
104115

116+
Then encode it to base64:
117+
118+
```javascript
119+
const config = {
120+
"accounts": [...]
121+
};
122+
const base64Config = Buffer.from(JSON.stringify(config)).toString('base64');
123+
console.log(base64Config);
124+
```
125+
126+
Set the base64 string in your `.env` file as `IMAP_CONFIG`.
127+
105128
## Security Considerations
106129

107130
### Open Source

lib/auth.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import {cookies} from "next/headers"
1515
import {SessionData} from "@/types/server";
1616
import {SecuritySettings} from "@/lib/settings";
1717

18-
// Master password hash stored in environment variable
19-
// Generate with: bcrypt.hash('your-password', 10)
20-
const MASTER_PASSWORD_BCRYPT_HASH = process.env.MASTER_PASSWORD_BCRYPT_HASH
18+
// Master password hash stored in environment variable as base64-encoded bcrypt hash
19+
// Generate with: Buffer.from(await bcrypt.hash('your-password', 10)).toString('base64')
20+
const MASTER_PASSWORD_BCRYPT_HASH_BASE64 = process.env.MASTER_PASSWORD_BCRYPT_HASH
21+
22+
// Decode the base64-encoded bcrypt hash at runtime
23+
const MASTER_PASSWORD_BCRYPT_HASH = MASTER_PASSWORD_BCRYPT_HASH_BASE64
24+
? Buffer.from(MASTER_PASSWORD_BCRYPT_HASH_BASE64, 'base64').toString('utf8')
25+
: undefined
2126

2227
// Validate environment variables at runtime, not at module load
2328
function validateEnvironment() {

lib/imap-config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ export function getImapConfig(): ImapConfig {
2424

2525
console.log("[IMAP CONFIG] Reading config from IMAP_CONFIG environment variable")
2626

27-
const configString = process.env.IMAP_CONFIG
27+
const configBase64 = process.env.IMAP_CONFIG
2828

29-
if (!configString) {
29+
if (!configBase64) {
3030
const error = new Error("IMAP_CONFIG environment variable is not set")
3131
console.error("[IMAP CONFIG] Failed to parse IMAP_CONFIG environment variable", {
3232
error: error.message,
@@ -38,7 +38,10 @@ export function getImapConfig(): ImapConfig {
3838

3939
let config: ImapConfig
4040
try {
41-
console.log("[IMAP CONFIG] Config environment variable read successfully")
41+
console.log("[IMAP CONFIG] Decoding base64-encoded config...")
42+
// Decode base64 to get JSON string
43+
const configString = Buffer.from(configBase64, 'base64').toString('utf8')
44+
console.log("[IMAP CONFIG] Config decoded successfully, parsing JSON...")
4245
config = JSON.parse(configString) as ImapConfig
4346
} catch (parseError) {
4447
console.error("[IMAP CONFIG] Failed to parse IMAP_CONFIG environment variable", {

scripts/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,13 @@ Enter password: ********
5050
Step 2: Generating bcrypt hash...
5151
✓ Hash generated successfully!
5252
53-
Step 3: Updating .env file...
53+
Step 3: Encoding hash to base64...
54+
✓ Hash encoded successfully!
55+
56+
Step 4: Updating .env file...
5457
✓ .env file updated successfully!
5558
56-
Step 4: Testing password verification...
59+
Step 5: Testing password verification...
5760
✓ Password verification successful!
5861
✓ .env file contains correct hash!
5962
✓ Password matches hash from .env!

scripts/setup-password.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* This script will:
77
* 1. Ask you for your desired master password
88
* 2. Generate a bcrypt hash
9-
* 3. Update the .env file automatically
10-
* 4. Test that everything works correctly
9+
* 3. Base64 encode the hash for storage
10+
* 4. Update the .env file automatically
11+
* 5. Test that everything works correctly
1112
*/
1213

1314
import bcrypt from 'bcryptjs';
@@ -94,10 +95,16 @@ async function main() {
9495
const hash = await bcrypt.hash(password, 10);
9596

9697
log('✓ Hash generated successfully!', 'green');
97-
log(`\nGenerated hash: ${colors.cyan}${hash}${colors.reset}\n`);
98+
log(`\nGenerated bcrypt hash: ${colors.cyan}${hash}${colors.reset}`);
9899

99-
// Step 4: Update .env file
100-
log('Step 4: Updating .env file...', 'bright');
100+
// Step 4: Base64 encode the hash
101+
log('\nStep 4: Encoding hash to base64...', 'bright');
102+
const base64Hash = Buffer.from(hash).toString('base64');
103+
log('✓ Hash encoded successfully!', 'green');
104+
log(`\nBase64-encoded hash: ${colors.cyan}${base64Hash}${colors.reset}\n`);
105+
106+
// Step 5: Update .env file
107+
log('Step 5: Updating .env file...', 'bright');
101108

102109
const envPath = path.join(__dirname, '..', '.env');
103110

@@ -111,17 +118,14 @@ async function main() {
111118
// Read current .env file
112119
let envContent = fs.readFileSync(envPath, 'utf8');
113120

114-
// Escape backslashes and $ characters to prevent unintended interpolation in .env files
115-
const escapedHash = hash.replace(/\\/g, '\\\\').replace(/\$/g, '\\$');
116-
117-
// Replace the hash line
121+
// Replace the hash line with base64-encoded hash
118122
const hashRegex = /^MASTER_PASSWORD_BCRYPT_HASH=.*/m;
119123
if (hashRegex.test(envContent)) {
120-
envContent = envContent.replace(hashRegex, `MASTER_PASSWORD_BCRYPT_HASH=${hash}`);
124+
envContent = envContent.replace(hashRegex, `MASTER_PASSWORD_BCRYPT_HASH=${base64Hash}`);
121125
log('✓ Found and updated existing MASTER_PASSWORD_BCRYPT_HASH', 'green');
122126
} else {
123127
// Add it if it doesn't exist
124-
envContent = `MASTER_PASSWORD_BCRYPT_HASH=${hash}\n` + envContent;
128+
envContent = `MASTER_PASSWORD_BCRYPT_HASH=${base64Hash}\n` + envContent;
125129
log('✓ Added MASTER_PASSWORD_BCRYPT_HASH to .env', 'green');
126130
}
127131

scripts/test-password.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* Usage: node scripts/test-password.js "your-password"
55
*
6-
* This will test your password against the hash in MASTER_PASSWORD_BCRYPT_HASH
6+
* This will test your password against the base64-encoded hash in MASTER_PASSWORD_BCRYPT_HASH
77
*/
88

99
const bcrypt = require('bcryptjs');
@@ -18,21 +18,27 @@ if (!password) {
1818
process.exit(1);
1919
}
2020

21-
const hash = process.env.MASTER_PASSWORD_BCRYPT_HASH;
21+
const base64Hash = process.env.MASTER_PASSWORD_BCRYPT_HASH;
2222

23-
if (!hash) {
23+
if (!base64Hash) {
2424
console.error('ERROR: MASTER_PASSWORD_BCRYPT_HASH not found in environment');
25-
console.error('Make sure you have a .env.local file with MASTER_PASSWORD_BCRYPT_HASH set');
25+
console.error('Make sure you have a .env file with MASTER_PASSWORD_BCRYPT_HASH set');
2626
process.exit(1);
2727
}
2828

2929
console.log('Testing password...');
3030
console.log('Password length:', password.length);
3131
console.log('Password (first 3 chars):', password.substring(0, 3) + '***');
32-
console.log('Hash exists:', !!hash);
33-
console.log('Hash starts with $2:', hash.startsWith('$2'));
34-
console.log('Hash length:', hash.length);
35-
console.log('Hash format:', hash.substring(0, 7) + '...');
32+
console.log('Base64 hash exists:', !!base64Hash);
33+
console.log('Base64 hash length:', base64Hash.length);
34+
console.log('Base64 hash (first 20 chars):', base64Hash.substring(0, 20) + '...');
35+
36+
// Decode the base64 hash
37+
const hash = Buffer.from(base64Hash, 'base64').toString('utf8');
38+
console.log('Decoded hash exists:', !!hash);
39+
console.log('Decoded hash starts with $2:', hash.startsWith('$2'));
40+
console.log('Decoded hash length:', hash.length);
41+
console.log('Decoded hash format:', hash.substring(0, 7) + '...');
3642

3743
bcrypt.compare(password, hash).then(result => {
3844
console.log('\n' + '='.repeat(50));

todo

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)