Skip to content

Commit 0b4ee1f

Browse files
author
Incharajayaram
committed
added base64 utils
1 parent ce275d6 commit 0b4ee1f

4 files changed

Lines changed: 266 additions & 25 deletions

File tree

frontend/lib/screens/compose_screen.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../app.dart';
88
import '../widgets/inbox_shell.dart';
99
import '../services/email_service.dart';
1010
import '../providers/auth_provider.dart';
11+
import '../utils/base64_utils.dart';
1112

1213
class ComposeScreen extends StatefulWidget {
1314
const ComposeScreen({super.key});
@@ -45,7 +46,8 @@ class _ComposeScreenState extends State<ComposeScreen> {
4546
_attachments.clear();
4647
for (final f in result.files) {
4748
if (f.bytes == null) continue;
48-
final b64 = base64Encode(f.bytes!);
49+
// Use utility function for consistent base64 encoding
50+
final b64 = Base64Utils.encode(f.bytes!);
4951
_attachments.add(SendAttachment(fileName: f.name, contentType: 'application/octet-stream', contentBase64: b64));
5052
}
5153
});

frontend/lib/screens/inbox_screen.dart

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:http/http.dart' as http;
1010
import '../services/email_service.dart';
1111
import '../providers/auth_provider.dart';
1212
import '../app.dart';
13+
import '../utils/base64_utils.dart';
1314

1415
class InboxScreen extends StatefulWidget {
1516
const InboxScreen({super.key});
@@ -434,33 +435,17 @@ class _InboxScreenState extends State<InboxScreen> {
434435
print('[_decodeBase64MaybeUrl] Input length: ${input.length}');
435436
print('[_decodeBase64MaybeUrl] Input preview: ${input.substring(0, input.length > 50 ? 50 : input.length)}');
436437

438+
// Use Base64Utils for consistent decoding
439+
print('[_decodeBase64MaybeUrl] Debug info: ${Base64Utils.getDebugInfo(input)}');
440+
437441
try {
438-
// Try standard base64 first
439-
print('[_decodeBase64MaybeUrl] Trying standard base64...');
440-
final result = Uint8List.fromList(base64Decode(input));
441-
print('[_decodeBase64MaybeUrl] ✅ Standard base64 decode succeeded, ${result.length} bytes');
442+
print('[_decodeBase64MaybeUrl] Using Base64Utils.safelyDecode...');
443+
final result = Base64Utils.safelyDecode(input);
444+
print('[_decodeBase64MaybeUrl] ✅ Decode succeeded, ${result.length} bytes');
442445
return result;
443446
} catch (e) {
444-
print('[_decodeBase64MaybeUrl] ❌ Standard base64 failed: $e');
445-
try {
446-
// Try base64url variant
447-
print('[_decodeBase64MaybeUrl] Trying base64url variant...');
448-
var s = input.replaceAll('-', '+').replaceAll('_', '/');
449-
switch (s.length % 4) {
450-
case 2:
451-
s += '==';
452-
break;
453-
case 3:
454-
s += '=';
455-
break;
456-
}
457-
final result = Uint8List.fromList(base64Decode(s));
458-
print('[_decodeBase64MaybeUrl] ✅ Base64url decode succeeded, ${result.length} bytes');
459-
return result;
460-
} catch (e2) {
461-
print('[_decodeBase64MaybeUrl] ❌ Base64url decode also failed: $e2');
462-
return null;
463-
}
447+
print('[_decodeBase64MaybeUrl] ❌ Decode failed: $e');
448+
return null;
464449
}
465450
}
466451

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import 'dart:convert';
2+
import 'dart:typed_data';
3+
4+
/// Base64 utility functions for consistent encoding/decoding across all encryption layers
5+
///
6+
/// These utilities ensure that base64 operations are performed correctly and consistently
7+
/// throughout the application, preventing double-encoding issues in PQC triple-layer encryption.
8+
class Base64Utils {
9+
/// Encodes bytes to standard base64 string
10+
///
11+
/// Use this for initial encoding of file data or any binary data
12+
static String encode(Uint8List bytes) {
13+
return base64Encode(bytes);
14+
}
15+
16+
/// Encodes UTF-8 string to base64
17+
///
18+
/// Use this when you need to encode text content
19+
static String encodeString(String text) {
20+
return base64Encode(utf8.encode(text));
21+
}
22+
23+
/// Decodes standard base64 string to bytes
24+
///
25+
/// Use this for decoding file data or any binary data
26+
/// Throws FormatException if the input is not valid base64
27+
static Uint8List decode(String base64String) {
28+
try {
29+
return base64Decode(base64String);
30+
} catch (e) {
31+
throw FormatException('Invalid base64 string: $e');
32+
}
33+
}
34+
35+
/// Decodes base64 string to UTF-8 string
36+
///
37+
/// Use this when you need to decode text content
38+
/// Throws FormatException if the input is not valid base64
39+
static String decodeToString(String base64String) {
40+
try {
41+
final bytes = base64Decode(base64String);
42+
return utf8.decode(bytes);
43+
} catch (e) {
44+
throw FormatException('Invalid base64 string or UTF-8 encoding: $e');
45+
}
46+
}
47+
48+
/// Validates if a string is valid base64
49+
///
50+
/// Returns true if the string is valid base64, false otherwise
51+
static bool isValidBase64(String value) {
52+
if (value.isEmpty) return false;
53+
54+
try {
55+
base64Decode(value);
56+
return true;
57+
} catch (e) {
58+
return false;
59+
}
60+
}
61+
62+
/// Converts base64url to standard base64
63+
///
64+
/// Base64url uses - and _ instead of + and /
65+
/// This is often used in URLs and tokens
66+
static String base64UrlToBase64(String base64Url) {
67+
// First restore any stripped padding
68+
String base64 = base64Url.replaceAll('-', '+').replaceAll('_', '/');
69+
70+
// Add padding if needed (base64url strips padding)
71+
while (base64.length % 4 != 0) {
72+
base64 += '=';
73+
}
74+
75+
return base64;
76+
}
77+
78+
/// Converts standard base64 to base64url
79+
///
80+
/// Removes padding and replaces + and / with - and _
81+
static String base64ToBase64Url(String base64) {
82+
return base64
83+
.replaceAll('+', '-')
84+
.replaceAll('/', '_')
85+
.replaceAll('=', '');
86+
}
87+
88+
/// Safely decodes base64, handling both standard and base64url formats
89+
///
90+
/// This is useful when you're not sure which format the input is in
91+
static Uint8List safelyDecode(String base64String) {
92+
try {
93+
// Try standard base64 first
94+
return base64Decode(base64String);
95+
} catch (e) {
96+
try {
97+
// Try base64url format
98+
final standardBase64 = base64UrlToBase64(base64String);
99+
return base64Decode(standardBase64);
100+
} catch (e2) {
101+
throw FormatException('Invalid base64 string (tried both standard and base64url): $e, $e2');
102+
}
103+
}
104+
}
105+
106+
/// Debug helper: Returns info about a base64 string
107+
///
108+
/// Useful for troubleshooting encoding issues
109+
static Map<String, dynamic> getDebugInfo(String base64String) {
110+
return {
111+
'length': base64String.length,
112+
'isValid': isValidBase64(base64String),
113+
'hasStandardChars': base64String.contains('+') || base64String.contains('/'),
114+
'hasUrlChars': base64String.contains('-') || base64String.contains('_'),
115+
'hasPadding': base64String.contains('='),
116+
'preview': base64String.length > 100
117+
? '${base64String.substring(0, 50)}...${base64String.substring(base64String.length - 50)}'
118+
: base64String,
119+
};
120+
}
121+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'dart:typed_data';
3+
import 'package:frontend/utils/base64_utils.dart';
4+
5+
void main() {
6+
group('Base64Utils Tests', () {
7+
test('encode and decode bytes correctly', () {
8+
// Create test data
9+
final testData = Uint8List.fromList([1, 2, 3, 4, 5, 255, 128, 64]);
10+
11+
// Encode
12+
final encoded = Base64Utils.encode(testData);
13+
print('Encoded: $encoded');
14+
15+
// Decode
16+
final decoded = Base64Utils.decode(encoded);
17+
print('Decoded: $decoded');
18+
19+
// Verify
20+
expect(decoded, equals(testData));
21+
});
22+
23+
test('encode and decode string correctly', () {
24+
const testString = 'Hello, World! This is a test with special chars: 你好世界 🚀';
25+
26+
// Encode
27+
final encoded = Base64Utils.encodeString(testString);
28+
print('Encoded string: $encoded');
29+
30+
// Decode
31+
final decoded = Base64Utils.decodeToString(encoded);
32+
print('Decoded string: $decoded');
33+
34+
// Verify
35+
expect(decoded, equals(testString));
36+
});
37+
38+
test('validate base64 strings', () {
39+
expect(Base64Utils.isValidBase64('SGVsbG8gV29ybGQ='), isTrue);
40+
expect(Base64Utils.isValidBase64('not-valid-base64!!!'), isFalse);
41+
expect(Base64Utils.isValidBase64(''), isFalse);
42+
});
43+
44+
test('convert between base64 and base64url', () {
45+
const base64 = 'SGVsbG8gV29ybGQ=';
46+
final base64url = Base64Utils.base64ToBase64Url(base64);
47+
print('Base64: $base64');
48+
print('Base64url: $base64url');
49+
50+
expect(base64url, equals('SGVsbG8gV29ybGQ'));
51+
52+
final backToBase64 = Base64Utils.base64UrlToBase64(base64url);
53+
expect(backToBase64, equals(base64));
54+
});
55+
56+
test('safely decode both base64 and base64url formats', () {
57+
final testData = Uint8List.fromList([255, 128, 64, 32]);
58+
59+
// Standard base64
60+
final standardBase64 = Base64Utils.encode(testData);
61+
final decoded1 = Base64Utils.safelyDecode(standardBase64);
62+
expect(decoded1, equals(testData));
63+
64+
// Base64url format
65+
final base64url = Base64Utils.base64ToBase64Url(standardBase64);
66+
final decoded2 = Base64Utils.safelyDecode(base64url);
67+
expect(decoded2, equals(testData));
68+
});
69+
70+
test('prevent double encoding issue', () {
71+
// Simulate what was happening in the old code
72+
final originalData = Uint8List.fromList([1, 2, 3, 4, 5]);
73+
74+
// First encoding (correct)
75+
final firstEncode = Base64Utils.encode(originalData);
76+
print('First encode: $firstEncode');
77+
78+
// WRONG: decode then re-encode (creates double encoding)
79+
final doubleEncoded = Base64Utils.encode(Base64Utils.decode(firstEncode));
80+
print('Double encoded: $doubleEncoded');
81+
82+
// They should be the same!
83+
expect(doubleEncoded, equals(firstEncode));
84+
85+
// Verify the decoded data is correct
86+
expect(Base64Utils.decode(firstEncode), equals(originalData));
87+
});
88+
89+
test('debug info provides useful information', () {
90+
const validBase64 = 'SGVsbG8gV29ybGQ=';
91+
const invalidBase64 = 'not-valid!!!';
92+
93+
final validInfo = Base64Utils.getDebugInfo(validBase64);
94+
print('Valid base64 info: $validInfo');
95+
expect(validInfo['isValid'], isTrue);
96+
expect(validInfo['hasPadding'], isTrue);
97+
98+
final invalidInfo = Base64Utils.getDebugInfo(invalidBase64);
99+
print('Invalid base64 info: $invalidInfo');
100+
expect(invalidInfo['isValid'], isFalse);
101+
});
102+
103+
test('simulate PQC attachment flow (no double encoding)', () {
104+
// Simulate picking a file
105+
final fileBytes = Uint8List.fromList(List.generate(1000, (i) => i % 256));
106+
print('Original file size: ${fileBytes.length} bytes');
107+
108+
// Step 1: Encode file to base64 (in compose_screen.dart)
109+
final contentBase64 = Base64Utils.encode(fileBytes);
110+
print('After base64 encode: ${contentBase64.length} chars');
111+
112+
// Step 2: In backend, use AsIs() - NO decode/encode!
113+
// This is what we fixed:
114+
// OLD: var bytes = Convert.FromBase64String(a.ContentBase64);
115+
// var contentText = Convert.ToBase64String(bytes);
116+
// NEW: var contentText = Base64Utils.AsIs(a.ContentBase64);
117+
118+
// Step 3: Backend encrypts the base64 string (simulated)
119+
final encrypted = 'ENCRYPTED_${contentBase64}_ENCRYPTED';
120+
121+
// Step 4: Backend decrypts (simulated)
122+
final decryptedBase64 = encrypted.substring(10, encrypted.length - 10);
123+
124+
// Step 5: Frontend decodes base64 to get original file
125+
final recoveredBytes = Base64Utils.decode(decryptedBase64);
126+
print('Recovered file size: ${recoveredBytes.length} bytes');
127+
128+
// Verify no data loss
129+
expect(recoveredBytes, equals(fileBytes));
130+
print('✅ PQC attachment flow test PASSED - no double encoding!');
131+
});
132+
});
133+
}

0 commit comments

Comments
 (0)