Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_entropy.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
26
27#include "nilorea/n_entropy.h"
28#include "nilorea/n_log.h"
29
30#include <math.h>
31#include <string.h>
32
33static int failures = 0;
34
35static void approx(const char* label, double got, double want, double tol) {
36 if (fabs(got - want) > tol) {
37 n_log(LOG_ERR, "%s: got %.4f, expected %.4f", label, got, want);
38 failures++;
39 }
40}
41
42int main(void) {
43 unsigned char uni[256];
44 unsigned char same[64];
45 unsigned char z[8];
46 unsigned char f[8];
47 unsigned char half[8];
48 unsigned char two[4] = {'A', 'B', 'A', 'B'};
49 int i;
51
52 /* a uniform byte histogram: max entropy, zero chi-square, balanced bits */
53 for (i = 0; i < 256; i++)
54 uni[i] = (unsigned char)i;
55 approx("uniform shannon", n_entropy_shannon(uni, 256), 8.0, 0.0001);
56 approx("uniform chi", n_entropy_chi_square(uni, 256), 0.0, 0.0001);
57 approx("uniform monobit", n_entropy_monobit(uni, 256), 0.5, 0.01);
58
59 /* a single repeated value: zero entropy, large chi-square */
60 memset(same, 'A', sizeof(same));
61 approx("same shannon", n_entropy_shannon(same, sizeof(same)), 0.0, 0.0001);
62 if (n_entropy_chi_square(same, sizeof(same)) <= 255.0) {
63 n_log(LOG_ERR, "same chi-square should be large");
64 failures++;
65 }
66
67 /* monobit on known bit patterns */
68 memset(z, 0x00, sizeof(z));
69 memset(f, 0xFF, sizeof(f));
70 memset(half, 0x0F, sizeof(half));
71 approx("zeros monobit", n_entropy_monobit(z, sizeof(z)), 0.0, 0.0001);
72 approx("ones monobit", n_entropy_monobit(f, sizeof(f)), 1.0, 0.0001);
73 approx("half monobit", n_entropy_monobit(half, sizeof(half)), 0.5, 0.0001);
74
75 /* two equally likely symbols: exactly one bit of entropy */
76 approx("two-symbol shannon", n_entropy_shannon(two, 4), 1.0, 0.0001);
77
78 /* empty / NULL samples are zero, not a crash */
79 approx("empty shannon", n_entropy_shannon(NULL, 0), 0.0, 0.0);
80 approx("empty chi", n_entropy_chi_square(z, 0), 0.0, 0.0);
81 approx("empty monobit", n_entropy_monobit(NULL, 0), 0.0, 0.0);
82
83 /* ---- the per-position and per-bit suite ---- */
84 {
85 /* 64 tokens, each 4 bytes: the first two positions are random, the third is
86 always the same value, and the fourth alternates between two values. That
87 is what a real weak token looks like, and what an aggregate metric hides:
88 the overall entropy stays high while two positions carry almost nothing. */
89 static unsigned char toks[64][4];
90 const unsigned char* ptrs[64];
91 N_ENTROPY_POS pos[4];
92 N_ENTROPY_BIT bits[32];
93 size_t t, np, nb;
94 unsigned int seed = 12345u;
95 for (t = 0; t < 64; t++) {
96 seed = seed * 1103515245u + 12345u;
97 toks[t][0] = (unsigned char)(seed >> 16);
98 seed = seed * 1103515245u + 12345u;
99 toks[t][1] = (unsigned char)(seed >> 16);
100 toks[t][2] = 0x42; /* stuck */
101 toks[t][3] = (unsigned char)((t % 2) ? 'a' : 'b'); /* two values */
102 ptrs[t] = toks[t];
103 }
104 np = n_entropy_positions(ptrs, 64, 4, pos, 4);
105 if (np != 4) {
106 n_log(LOG_ERR, "positions: expected 4, got %zu", np);
107 failures++;
108 }
109 /* the stuck position is the finding: one value, zero entropy */
110 if (pos[2].distinct != 1) {
111 n_log(LOG_ERR, "position 2 should have a single value, got %zu", pos[2].distinct);
112 failures++;
113 }
114 approx("stuck position entropy", pos[2].shannon, 0.0, 0.0001);
115 /* the two-value position carries exactly one bit */
116 if (pos[3].distinct != 2) {
117 n_log(LOG_ERR, "position 3 should have two values, got %zu", pos[3].distinct);
118 failures++;
119 }
120 approx("two-value position entropy", pos[3].shannon, 1.0, 0.0001);
121 /* while the random positions carry many values and much more entropy */
122 if (pos[0].distinct < 30 || pos[0].shannon < 4.0) {
123 n_log(LOG_ERR, "position 0 should look random: %zu values, %.2f bits", pos[0].distinct, pos[0].shannon);
124 failures++;
125 }
126
127 nb = n_entropy_bits(ptrs, 64, 4, bits, 32);
128 if (nb != 32) {
129 n_log(LOG_ERR, "bits: expected 32, got %zu", nb);
130 failures++;
131 }
132 /* 0x42 is 0100 0010: bit 16 of the token (the top bit of byte 2) is always 0
133 and bit 17 always 1, so both are stuck and must be flagged */
134 if (bits[16].ones != 0 || bits[17].ones != 64) {
135 n_log(LOG_ERR, "stuck bits misreported: %zu / %zu", bits[16].ones, bits[17].ones);
136 failures++;
137 }
138 if (bits[17].p_value > 0.01) {
139 n_log(LOG_ERR, "a stuck bit should be significant, p=%.4f", bits[17].p_value);
140 failures++;
141 }
142 }
143
144 /* the chi-square p-value: a statistic equal to its degrees of freedom is
145 unremarkable, a huge one is not */
146 {
147 double p_mid = n_entropy_chi_square_p(10.0, 10);
148 double p_big = n_entropy_chi_square_p(200.0, 10);
149 if (p_mid < 0.3 || p_mid > 0.7) {
150 n_log(LOG_ERR, "chi p(10,10) should be mid-range, got %.4f", p_mid);
151 failures++;
152 }
153 if (p_big > 1e-6) {
154 n_log(LOG_ERR, "chi p(200,10) should be tiny, got %.6f", p_big);
155 failures++;
156 }
157 approx("chi p of zero", n_entropy_chi_square_p(0.0, 10), 1.0, 0.0001);
158 approx("chi p with no df", n_entropy_chi_square_p(50.0, 0), 1.0, 0.0001);
159 }
160
161 /* the runs test catches what monobit cannot: 0x0F repeated has exactly half its
162 bits set (monobit is perfect) but they come in long blocks */
163 {
164 double p_runs = n_entropy_runs_p(half, sizeof(half));
165 approx("blocky monobit is perfect", n_entropy_monobit(half, sizeof(half)), 0.5, 0.0001);
166 if (p_runs > 0.01) {
167 n_log(LOG_ERR, "runs test should reject a blocky stream, p=%.4f", p_runs);
168 failures++;
169 }
170 if (n_entropy_longest_run(half, sizeof(half)) != 4) {
171 n_log(LOG_ERR, "longest run of 0x0F should be 4, got %zu", n_entropy_longest_run(half, sizeof(half)));
172 failures++;
173 }
174 if (n_entropy_longest_run(f, sizeof(f)) != sizeof(f) * 8) {
175 n_log(LOG_ERR, "all-ones longest run should span the sample");
176 failures++;
177 }
178 }
179
180 /* guards: NULL and zero-sized inputs answer, they do not crash */
181 {
182 N_ENTROPY_POS pos1[1];
183 N_ENTROPY_BIT bit1[1];
184 const unsigned char* one[1];
185 one[0] = z;
186 if (n_entropy_positions(NULL, 1, 1, pos1, 1) != 0) failures++;
187 if (n_entropy_positions(one, 0, 1, pos1, 1) != 0) failures++;
188 if (n_entropy_positions(one, 1, 0, pos1, 1) != 0) failures++;
189 if (n_entropy_positions(one, 1, 1, NULL, 1) != 0) failures++;
190 if (n_entropy_bits(NULL, 1, 1, bit1, 1) != 0) failures++;
191 if (n_entropy_bits(one, 1, 1, NULL, 1) != 0) failures++;
192 approx("runs of an empty sample", n_entropy_runs_p(NULL, 0), 0.0, 0.0);
193 if (n_entropy_longest_run(NULL, 0) != 0) failures++;
194 }
195
196 if (failures) {
197 n_log(LOG_ERR, "ex_entropy: %d failure(s)", failures);
198 return 1;
199 }
200 n_log(LOG_NOTICE, "ex_entropy: all checks passed");
201 return 0;
202}
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void approx(const char *label, double got, double want, double tol)
Definition ex_entropy.c:35
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
size_t n_entropy_positions(const unsigned char *const *samples, size_t count, size_t sample_len, N_ENTROPY_POS *out, size_t max_out)
Per-POSITION analysis of a set of equal-length samples.
Definition n_entropy.c:94
size_t n_entropy_bits(const unsigned char *const *samples, size_t count, size_t sample_len, N_ENTROPY_BIT *out, size_t max_out)
Per-BIT analysis of a set of equal-length samples.
Definition n_entropy.c:133
size_t n_entropy_longest_run(const unsigned char *data, size_t len)
Length of the longest run of identical bits in the sample.
Definition n_entropy.c:183
double n_entropy_shannon(const unsigned char *data, size_t len)
Shannon entropy of the byte sample, in bits per byte (0.0 .
Definition n_entropy.c:29
double n_entropy_monobit(const unsigned char *data, size_t len)
Fraction of set bits in the sample (0.0 .
Definition n_entropy.c:48
double n_entropy_chi_square(const unsigned char *data, size_t len)
Chi-square statistic of the byte histogram against a uniform distribution over 256 values.
Definition n_entropy.c:64
double n_entropy_chi_square_p(double chi, size_t df)
Upper-tail p-value of a chi-square statistic (Wilson-Hilferty).
Definition n_entropy.c:82
double n_entropy_runs_p(const unsigned char *data, size_t len)
Runs test over the sample's bit stream (NIST SP 800-22 style).
Definition n_entropy.c:159
per-bit statistics across a set of equal-length samples
Definition n_entropy.h:74
per-position statistics across a set of equal-length samples
Definition n_entropy.h:66
Randomness/entropy metrics for byte samples (token-randomness analysis).
Generic log system.