Skip to content

Commit 6b832b8

Browse files
committed
Add keyslot check code.
This patch adds keyslot randomness analysis to cryptsetup repair command to check for a detectable corruption of binary area. It uses Chi2 analysis. This check basically replaces external keyslot checker program.
1 parent f29337a commit 6b832b8

7 files changed

Lines changed: 227 additions & 0 deletions

File tree

man/cryptsetup-repair.8.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ reencryption recovery so that reencryption can continue later.
3333
Repairing reencryption requires verification of reencryption
3434
keyslot so passphrase or keyfile is needed.
3535

36+
=== LUKS keyslots corruption detection
37+
38+
The repair command also checks for detectable corruption of keyslot
39+
content. Corruption of a keyslot results in a situation when a known
40+
password is no longer accepted. It can happen due to storage media
41+
failure or overwriting the keyslot area by some other data.
42+
Only certain corruptions, usually only a low-entropy area
43+
(like zeroed blocks), can be detected.
44+
45+
The detection prints only warnings. It does not modify keyslots.
46+
It can also print more specific offsets on the device for detailed
47+
manual inspection.
48+
49+
Please note that the warning can be a false positive
50+
(no real corruption happened).
51+
Conversely, if the keyslot is corrupted, no recovery is possible.
52+
You have to use LUKS header backup.
53+
54+
3655
*<options>* can be [--timeout, --verify-passphrase, --disable-locks,
3756
--type, --header, --key-file, --keyfile-size, --keyfile-offset, --key-slot].
3857

po/POTFILES.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@ src/utils_reencrypt_luks1.c
5454
src/utils_blockdev.c
5555
src/utils_args.c
5656
src/utils_key_description.c
57+
src/utils_keyslot_check.c
5758
tokens/ssh/cryptsetup-ssh.c
5859
tokens/ssh/ssh-utils.c

src/Makemodule.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cryptsetup_SOURCES = \
1818
src/utils_reencrypt_luks1.c \
1919
src/utils_progress.c \
2020
src/utils_key_description.c \
21+
src/utils_keyslot_check.c \
2122
src/cryptsetup.c \
2223
src/cryptsetup.h \
2324
src/cryptsetup_args.h \

src/cryptsetup.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,6 +1376,10 @@ static int action_luksRepair(void)
13761376
if (!r && isLUKS2(crypt_get_type(cd)))
13771377
r = luks2_reencrypt_repair(cd);
13781378

1379+
/* Randomness analysis of LUKS keyslot binary data, this is only a hint */
1380+
if (r == 0)
1381+
luks_check_keyslots(cd, header_device);
1382+
13791383
crypt_free(cd);
13801384
return r;
13811385
}

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ if get_option('cryptsetup')
1212
'utils_reencrypt_luks1.c',
1313
'utils_tools.c',
1414
'utils_key_description.c',
15+
'utils_keyslot_check.c',
1516
)
1617
cryptsetup_files += lib_tools_files
1718
cryptsetup_deps = [

src/utils_keyslot_check.c

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
/*
3+
* cryptsetup - keyslot randomness check
4+
*
5+
* Copyright (C) 2024-2025 Milan Broz
6+
*/
7+
8+
#include <math.h>
9+
#include "cryptsetup.h"
10+
#include "utils_luks.h"
11+
12+
static uint64_t bitcount(uint64_t *p, uint64_t count64)
13+
{
14+
uint64_t i, l, s = 0;
15+
16+
for (i = 0; i < count64; i++) {
17+
l = *p++;
18+
19+
if (l == 0)
20+
continue;
21+
22+
l = (l & 0x5555555555555555ULL) + ((l >> 1) & 0x5555555555555555ULL);
23+
l = (l & 0x3333333333333333ULL) + ((l >> 2) & 0x3333333333333333ULL);
24+
l = (l & 0x0f0f0f0f0f0f0f0fULL) + ((l >> 4) & 0x0f0f0f0f0f0f0f0fULL);
25+
l = (l & 0x00ff00ff00ff00ffULL) + ((l >> 8) & 0x00ff00ff00ff00ffULL);
26+
l = (l & 0x0000ffff0000ffffULL) + ((l >> 16) & 0x0000ffff0000ffffULL);
27+
l = (l & 0x00000000ffffffffULL) + ((l >> 32) & 0x00000000ffffffffULL);
28+
29+
s += l;
30+
}
31+
32+
return s;
33+
}
34+
35+
static double chisquared_bits(void *b, uint64_t count)
36+
{
37+
size_t i;
38+
double f[2] = {0};
39+
double tmp, t = 0, e = count * 8 / ARRAY_SIZE(f);
40+
41+
f[1] = bitcount((uint64_t*)b, count / sizeof(uint64_t));
42+
f[0] = (count * 8) - f[1];
43+
44+
for (i = 0; i < ARRAY_SIZE(f); i++) {
45+
/* t += pow(f[i] - e, 2) / e; */
46+
tmp = f[i] - e;
47+
tmp = tmp * tmp;
48+
tmp = tmp / e;
49+
t += tmp;
50+
}
51+
52+
return t;
53+
}
54+
55+
static double chisquared_bytes(unsigned char *N, uint64_t count)
56+
{
57+
size_t i;
58+
double f[256] = {0};
59+
double tmp, t = 0, e = count / ARRAY_SIZE(f);
60+
61+
for (i = 0; i < count; i++)
62+
f[N[i]]++;
63+
64+
for (i = 0; i < ARRAY_SIZE(f); i++) {
65+
/* t += pow(f[i] - e, 2) / e; */
66+
tmp = f[i] - e;
67+
tmp = tmp * tmp;
68+
tmp = tmp / e;
69+
t += tmp;
70+
}
71+
72+
return t;
73+
}
74+
75+
/*
76+
* The keyslot area is encrypted, it should contain pseudorandom data.
77+
* This function performs randomness analysis to detect a possible overwrite
78+
* with a low-entropy data (causing keyslot corruption).
79+
* To limit false positives, it performs these checks:
80+
* - splits the keyslot area to 4096-byte blocks
81+
* - if the last block is a partial, it uses the last 4096 bytes instead
82+
* - with 4096-byte blocks, run Chi-squared test (alpha 0.001 with right tail only)
83+
* that expects uniform bytes distribution
84+
* - if the test value is larger than the critical value,
85+
* it tries to search in 128-byte subblocks
86+
* - with 128-byte blocks, run Chi-squared test (alpha 0.0001 with right tail only)
87+
* that expects uniform bits distribution
88+
*
89+
* Note: this test cannot detect all corruptions and can produce false positives.
90+
* It is just a hint for the user.
91+
*/
92+
static void run_analysis(int keyslot, unsigned char *buffer,
93+
uint64_t length, uint64_t start,
94+
bool *hexdump_hint, int hint_max)
95+
{
96+
const unsigned int BLOCK = 4096, SUBBLOCK = 128;
97+
uint64_t ofs, ofs2;
98+
bool suspected = false;
99+
int hint_count;
100+
101+
log_dbg("Keyslot %d [0x%06" PRIx64 " - 0x%06" PRIx64 "] randomness analysis", keyslot, start, start + length);
102+
103+
for (ofs = 0, hint_count = 0; ofs < length; ofs += BLOCK) {
104+
105+
/* For the last incomplete block just use the last BLOCK bytes */
106+
if ((ofs + BLOCK) > length)
107+
ofs = length - BLOCK;
108+
109+
/* Chi-squared: 256 buckets (bytes), alpha 0.001, right tail */
110+
if (chisquared_bytes(buffer + ofs, BLOCK) <= 330.5197)
111+
continue;
112+
113+
if (!suspected)
114+
log_std(_("Keyslot %d binary data could be corrupted.\n"), keyslot);
115+
suspected = true;
116+
117+
for (ofs2 = 0; ofs2 < BLOCK && hint_count < hint_max; ofs2 += SUBBLOCK) {
118+
119+
/* Chi-squared: 2 buckets (bits), alpha 0.0001, right tail */
120+
if (chisquared_bits(buffer + ofs + ofs2, SUBBLOCK) <= 15.1367)
121+
continue;
122+
123+
if (hint_count < hint_max)
124+
log_std(_(" Suspected offset: 0x%" PRIx64 "\n"), start + ofs + ofs2);
125+
*hexdump_hint = true;
126+
if (++hint_count == hint_max)
127+
log_std(_(" Subsequent suspected offsets are suppressed.\n"), start + ofs + ofs2);
128+
}
129+
}
130+
}
131+
132+
void luks_check_keyslots(struct crypt_device *cd, const char *device)
133+
{
134+
crypt_keyslot_info ki;
135+
unsigned char *buffer = NULL;
136+
uint64_t start, length, data_length;
137+
int i, fd = -1, r = -EINVAL;
138+
bool hexdump_hint = false;
139+
140+
if (crypt_reencrypt_status(cd, NULL) != CRYPT_REENCRYPT_NONE) {
141+
log_dbg("In reencryption, skipping keyslots randomness test.");
142+
return;
143+
}
144+
145+
fd = open(device, O_RDONLY);
146+
if (fd == -1)
147+
return;
148+
149+
for (i = 0; i < crypt_keyslot_max(crypt_get_type(cd)); i++) {
150+
151+
ki = crypt_keyslot_status(cd, i);
152+
if (ki <= CRYPT_SLOT_INACTIVE || ki == CRYPT_SLOT_UNBOUND)
153+
continue;
154+
155+
r = crypt_keyslot_area(cd, i, &start, &length);
156+
if (r < 0)
157+
goto out;
158+
159+
r = crypt_keyslot_get_key_size(cd, i);
160+
if (r < 0)
161+
goto out;
162+
163+
/* unbound key or something we should not run randomness analysis */
164+
if (r <= 1)
165+
continue;
166+
167+
/*
168+
* data_length is the really used keyslot area
169+
* length = data_legth + padding_4096
170+
*/
171+
data_length = r * LUKS_STRIPES;
172+
173+
buffer = malloc(data_length);
174+
if (!buffer)
175+
goto out;
176+
177+
if (lseek(fd, (off_t)start, SEEK_SET) == -1) {
178+
log_err(_("Keyslot %d cannot be read from the device."), i);
179+
goto out;
180+
}
181+
182+
if (read_buffer(fd, buffer, data_length) != (ssize_t)data_length) {
183+
log_err(_("Keyslot %d cannot be read from the device."), i);
184+
goto out;
185+
}
186+
187+
run_analysis(i, buffer, data_length, start, &hexdump_hint, 3);
188+
189+
free(buffer);
190+
buffer = NULL;
191+
}
192+
193+
if (hexdump_hint)
194+
log_std(_("You can use hexdump -v -C -n 128 -s <offset_0xXXXX> \"%s\" to inspect the data.\n"), device);
195+
out:
196+
if (fd != -1)
197+
close(fd);
198+
free(buffer);
199+
}

src/utils_luks.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,6 @@ int luks_init_keyslot_contexts_by_volume_keys(struct crypt_device *cd,
6666
struct crypt_keyslot_context **r_kc1,
6767
struct crypt_keyslot_context **r_kc2);
6868

69+
void luks_check_keyslots(struct crypt_device *cd, const char *device);
70+
6971
#endif /* UTILS_LUKS_H */

0 commit comments

Comments
 (0)