Skip to content

Commit cbbd11c

Browse files
committed
Harden scalar encode alphabet mapping
1 parent 2a1f5d8 commit cbbd11c

4 files changed

Lines changed: 68 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Added checked encoded-length helpers.
1515
- Added exact decoded-length helpers.
1616
- Hardened decode errors to report absolute input indexes.
17+
- Hardened scalar encode to avoid input-derived alphabet table indexes.
1718
- Hardened alphabet decode to avoid branch-heavy match ladders.
1819
- Hardened `decode_vec` to validate input before allocating decoded output.
1920
- Hardened stream decoders to preserve reader boundaries after terminal padding.

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,10 @@ Security commitments:
180180
- No unsafe code in scalar code.
181181
- Future unsafe SIMD isolated under `src/simd/`.
182182
- Strict decoding rejects malformed padding and trailing data.
183-
- Alphabet decode uses branch-minimized scalar arithmetic, but strict decoding is not documented as a cryptographic constant-time API.
183+
- Scalar encode avoids input-derived alphabet table indexes, and scalar decode
184+
uses branch-minimized arithmetic. These paths are hardened against obvious
185+
timing pitfalls, but they are not documented as formally verified
186+
cryptographic constant-time APIs.
184187
- Legacy compatibility must be opt-in.
185188
- Release gates include formatting, clippy, tests, Miri when installed, docs, dependency policy, audit, license review, SBOM, and reproducible build checks.
186189
- Future Kani proofs target in-place decoding bounds and scalar decoder invariants.

SECURITY.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ The local release gate skips only the large deterministic sweep tests under
3636
Miri. Those tests still run in the normal stable test suite; Miri focuses on the
3737
scalar and in-place safety surface that benefits most from interpreter checks.
3838

39-
The scalar decoder avoids obvious alphabet `match` ladders by using
40-
branch-minimized arithmetic for ASCII classification. This reduces easy
41-
data-dependent branching, but `base64-ng` does not currently claim a formally
42-
verified cryptographic constant-time decode API.
39+
The scalar encoder avoids input-derived alphabet table indexes, and the scalar
40+
decoder avoids obvious alphabet `match` ladders by using branch-minimized
41+
arithmetic for ASCII classification. This reduces easy timing pitfalls, but
42+
`base64-ng` does not currently claim a formally verified cryptographic
43+
constant-time encode or decode API.
4344

4445
Required before unsafe SIMD stabilizes:
4546

src/lib.rs

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ impl Alphabet for Standard {
790790

791791
#[inline]
792792
fn decode(byte: u8) -> Option<u8> {
793-
decode_ascii_base64(byte, b'+', b'/')
793+
decode_ascii_base64(byte, Self::ENCODE[62], Self::ENCODE[63])
794794
}
795795
}
796796

@@ -803,10 +803,30 @@ impl Alphabet for UrlSafe {
803803

804804
#[inline]
805805
fn decode(byte: u8) -> Option<u8> {
806-
decode_ascii_base64(byte, b'-', b'_')
806+
decode_ascii_base64(byte, Self::ENCODE[62], Self::ENCODE[63])
807807
}
808808
}
809809

810+
#[inline]
811+
const fn encode_base64_value<A: Alphabet>(value: u8) -> u8 {
812+
encode_ascii_base64(value, A::ENCODE[62], A::ENCODE[63])
813+
}
814+
815+
#[inline]
816+
const fn encode_ascii_base64(value: u8, value_62_byte: u8, value_63_byte: u8) -> u8 {
817+
let upper = mask_if(value < 26);
818+
let lower = mask_if(value.wrapping_sub(26) < 26);
819+
let digit = mask_if(value.wrapping_sub(52) < 10);
820+
let value_62 = mask_if(value == 0x3e);
821+
let value_63 = mask_if(value == 0x3f);
822+
823+
(value.wrapping_add(b'A') & upper)
824+
| (value.wrapping_sub(26).wrapping_add(b'a') & lower)
825+
| (value.wrapping_sub(52).wrapping_add(b'0') & digit)
826+
| (value_62_byte & value_62)
827+
| (value_63_byte & value_63)
828+
}
829+
810830
#[inline]
811831
fn decode_ascii_base64(byte: u8, value_62_byte: u8, value_63_byte: u8) -> Option<u8> {
812832
let upper = mask_if(byte.wrapping_sub(b'A') <= b'Z' - b'A');
@@ -826,8 +846,8 @@ fn decode_ascii_base64(byte: u8, value_62_byte: u8, value_63_byte: u8) -> Option
826846
}
827847

828848
#[inline]
829-
fn mask_if(condition: bool) -> u8 {
830-
0u8.wrapping_sub(u8::from(condition))
849+
const fn mask_if(condition: bool) -> u8 {
850+
0u8.wrapping_sub(condition as u8)
831851
}
832852

833853
/// A zero-sized Base64 engine parameterized by alphabet and padding policy.
@@ -920,10 +940,10 @@ where
920940
let b1 = input[read + 1];
921941
let b2 = input[read + 2];
922942

923-
output[write] = A::ENCODE[(b0 >> 2) as usize];
924-
output[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
925-
output[write + 2] = A::ENCODE[(((b1 & 0b0000_1111) << 2) | (b2 >> 6)) as usize];
926-
output[write + 3] = A::ENCODE[(b2 & 0b0011_1111) as usize];
943+
output[write] = encode_base64_value::<A>(b0 >> 2);
944+
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
945+
output[write + 2] = encode_base64_value::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
946+
output[write + 3] = encode_base64_value::<A>(b2 & 0b0011_1111);
927947

928948
read += 3;
929949
write += 4;
@@ -933,8 +953,8 @@ where
933953
0 => {}
934954
1 => {
935955
let b0 = input[read];
936-
output[write] = A::ENCODE[(b0 >> 2) as usize];
937-
output[write + 1] = A::ENCODE[((b0 & 0b0000_0011) << 4) as usize];
956+
output[write] = encode_base64_value::<A>(b0 >> 2);
957+
output[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
938958
write += 2;
939959
if PAD {
940960
output[write] = b'=';
@@ -944,9 +964,9 @@ where
944964
2 => {
945965
let b0 = input[read];
946966
let b1 = input[read + 1];
947-
output[write] = A::ENCODE[(b0 >> 2) as usize];
948-
output[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
949-
output[write + 2] = A::ENCODE[((b1 & 0b0000_1111) << 2) as usize];
967+
output[write] = encode_base64_value::<A>(b0 >> 2);
968+
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
969+
output[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
950970
if PAD {
951971
output[write + 3] = b'=';
952972
}
@@ -974,10 +994,10 @@ where
974994
let b1 = input[read + 1];
975995
let b2 = input[read + 2];
976996

977-
output[write] = A::ENCODE[(b0 >> 2) as usize];
978-
output[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
979-
output[write + 2] = A::ENCODE[(((b1 & 0b0000_1111) << 2) | (b2 >> 6)) as usize];
980-
output[write + 3] = A::ENCODE[(b2 & 0b0011_1111) as usize];
997+
output[write] = encode_base64_value::<A>(b0 >> 2);
998+
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
999+
output[write + 2] = encode_base64_value::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
1000+
output[write + 3] = encode_base64_value::<A>(b2 & 0b0011_1111);
9811001

9821002
read += 3;
9831003
write += 4;
@@ -987,8 +1007,8 @@ where
9871007
0 => {}
9881008
1 => {
9891009
let b0 = input[read];
990-
output[write] = A::ENCODE[(b0 >> 2) as usize];
991-
output[write + 1] = A::ENCODE[((b0 & 0b0000_0011) << 4) as usize];
1010+
output[write] = encode_base64_value::<A>(b0 >> 2);
1011+
output[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
9921012
write += 2;
9931013
if PAD {
9941014
output[write] = b'=';
@@ -999,9 +1019,9 @@ where
9991019
2 => {
10001020
let b0 = input[read];
10011021
let b1 = input[read + 1];
1002-
output[write] = A::ENCODE[(b0 >> 2) as usize];
1003-
output[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
1004-
output[write + 2] = A::ENCODE[((b1 & 0b0000_1111) << 2) as usize];
1022+
output[write] = encode_base64_value::<A>(b0 >> 2);
1023+
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1024+
output[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
10051025
write += 3;
10061026
if PAD {
10071027
output[write] = b'=';
@@ -1093,14 +1113,14 @@ where
10931113
let b0 = buffer[read];
10941114
if PAD {
10951115
write -= 4;
1096-
buffer[write] = A::ENCODE[(b0 >> 2) as usize];
1097-
buffer[write + 1] = A::ENCODE[((b0 & 0b0000_0011) << 4) as usize];
1116+
buffer[write] = encode_base64_value::<A>(b0 >> 2);
1117+
buffer[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
10981118
buffer[write + 2] = b'=';
10991119
buffer[write + 3] = b'=';
11001120
} else {
11011121
write -= 2;
1102-
buffer[write] = A::ENCODE[(b0 >> 2) as usize];
1103-
buffer[write + 1] = A::ENCODE[((b0 & 0b0000_0011) << 4) as usize];
1122+
buffer[write] = encode_base64_value::<A>(b0 >> 2);
1123+
buffer[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
11041124
}
11051125
}
11061126
2 => {
@@ -1109,15 +1129,17 @@ where
11091129
let b1 = buffer[read + 1];
11101130
if PAD {
11111131
write -= 4;
1112-
buffer[write] = A::ENCODE[(b0 >> 2) as usize];
1113-
buffer[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
1114-
buffer[write + 2] = A::ENCODE[((b1 & 0b0000_1111) << 2) as usize];
1132+
buffer[write] = encode_base64_value::<A>(b0 >> 2);
1133+
buffer[write + 1] =
1134+
encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1135+
buffer[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
11151136
buffer[write + 3] = b'=';
11161137
} else {
11171138
write -= 3;
1118-
buffer[write] = A::ENCODE[(b0 >> 2) as usize];
1119-
buffer[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
1120-
buffer[write + 2] = A::ENCODE[((b1 & 0b0000_1111) << 2) as usize];
1139+
buffer[write] = encode_base64_value::<A>(b0 >> 2);
1140+
buffer[write + 1] =
1141+
encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1142+
buffer[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
11211143
}
11221144
}
11231145
_ => unreachable!(),
@@ -1130,10 +1152,10 @@ where
11301152
let b1 = buffer[read + 1];
11311153
let b2 = buffer[read + 2];
11321154

1133-
buffer[write] = A::ENCODE[(b0 >> 2) as usize];
1134-
buffer[write + 1] = A::ENCODE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize];
1135-
buffer[write + 2] = A::ENCODE[(((b1 & 0b0000_1111) << 2) | (b2 >> 6)) as usize];
1136-
buffer[write + 3] = A::ENCODE[(b2 & 0b0011_1111) as usize];
1155+
buffer[write] = encode_base64_value::<A>(b0 >> 2);
1156+
buffer[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1157+
buffer[write + 2] = encode_base64_value::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
1158+
buffer[write + 3] = encode_base64_value::<A>(b2 & 0b0011_1111);
11371159
}
11381160

11391161
debug_assert_eq!(write, 0);

0 commit comments

Comments
 (0)