xxtea 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52dd181dad48d0089d4f84d4b715b0c7231ea004c35928e4ec5c154b5c19a569
4
- data.tar.gz: b60c3915ae8573e6d234cd6e9315f48ccb893c535d78ed3defd5f61428b4c825
3
+ metadata.gz: 89a73616644886a2d6c15b0567b6a123ba8029a410affd9aa7c76213358ba5b9
4
+ data.tar.gz: 181fff72b4214e44978ccd5ad26209d1362b0dcccaa949944bcf8e29c14b3577
5
5
  SHA512:
6
- metadata.gz: a49001c4bb5c6e53a495bf3a96ecc61d084bb822a77dfda6370dd432b5f50ae54321ffbec3736cc1773fc68059465cb42aeedb3db2511b54c86f44ba390976f0
7
- data.tar.gz: a9671360c531507c76ddec41561817312e4f0c961ef1bc6865b6e2ae5ff1ac39eab35d990ca1cab69317992e815e34277e039b60cab7b6b692aa20c550f51731
6
+ metadata.gz: 5dc86b066d959b3d9bbb013fa9f3ba7cd772608cc5dcb434889185c8a0fccb4ab0b01f210fa3c628745d6efd9e760a29e175d469febb2d4618d61e62e13b0ad8
7
+ data.tar.gz: 804d0e50a19dcfcfbfb087376612e4d7f92309b9192463e324b92e133cf291466f0812127b6dabe22927bb48bb160032290beea8e596f3be4b66f25374c2c42e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ - Add the `:length_word_suffix` and `:length_word_prefix` padding schemes, matching all five padding modes of [Python xxtea](https://github.com/ifduyue/xxtea) 6.2.0.
6
+ - `:length_word_suffix`: zero-pads to a 4-byte boundary, then appends one little-endian `uint32` with the original length (Cocos Creator JSC files).
7
+ - `:length_word_prefix`: prepends one little-endian `uint32` with the original length, then zero-pads the data to a 4-byte boundary.
8
+ - Both support empty input (padded to XXTEA's 2-word minimum) and raise `ArgumentError` on decrypt when the zero padding or length word is inconsistent.
9
+ - Add `XXTEA::LENGTH_WORD_SUFFIX` and `XXTEA::LENGTH_WORD_PREFIX` constant aliases.
10
+
3
11
  ## 1.1.0
4
12
 
5
13
  - Add named padding schemes: `:pkcs7_4_min8` (default, also `true`), `:pkcs7_8`, and `:none` (also `false`).
data/README.md CHANGED
@@ -91,6 +91,7 @@ c = XXTEA.new(key) # rounds=0, padding=:pkcs7_4_min8
91
91
  c = XXTEA.new(key, rounds: 64) # override rounds
92
92
  c = XXTEA.new(key, padding: false) # disable padding
93
93
  c = XXTEA.new(key, padding: :pkcs7_8) # 8-byte PKCS#7
94
+ c = XXTEA.new(key, padding: :length_word_suffix) # length word + zero padding
94
95
  c = XXTEA.new(key, padding: false, rounds: 42)
95
96
  ```
96
97
 
@@ -110,9 +111,12 @@ s == XXTEA.decrypt([hexenc].pack("H*"), key) # => true
110
111
  | --- | --- |
111
112
  | `true` or `:pkcs7_4_min8` (default) | 4-byte PKCS#7-like, padded to at least 8 bytes. Compatible with Python xxtea. Not standard 4-byte PKCS#7 |
112
113
  | `:pkcs7_8` | Standard **8-byte** PKCS#7, compatible with Python xxteang |
114
+ | `:length_word_suffix` | Zero-pad to a 4-byte boundary, then append one little-endian `uint32` with the original length. Cocos Creator JSC files using this layout can be decrypted. Compatible with Python xxtea 6.2.0 |
115
+ | `:length_word_prefix` | Prepend one little-endian `uint32` with the original length, then zero-pad the data to a 4-byte boundary. Compatible with Python xxtea 6.2.0 |
113
116
  | `false` or `:none` | No padding (raw XXTEA) |
114
117
 
115
- `XXTEA::PKCS7_4_MIN8` and `XXTEA::PKCS7_8` are aliases for the symbols.
118
+ `XXTEA::PKCS7_4_MIN8`, `XXTEA::PKCS7_8`, `XXTEA::LENGTH_WORD_PREFIX`, and
119
+ `XXTEA::LENGTH_WORD_SUFFIX` are aliases for the symbols.
116
120
 
117
121
  The default `:pkcs7_4_min8` scheme uses pad byte value `4 - (data.bytesize & 3)`
118
122
  (range 1–4), plus an extra 4 bytes when the input is shorter than 4 bytes
@@ -123,12 +127,21 @@ byte, encrypting an 8-byte input produces a 12-byte ciphertext.
123
127
  8-byte PKCS#7 uses pad byte value `8 - (data.bytesize & 7)` (range 1–8).
124
128
  Encrypting an 8-byte input produces a 16-byte ciphertext.
125
129
 
130
+ The length-word schemes store the original byte length in a little-endian
131
+ `uint32` word, so the plaintext length must fit in 32 bits (`RangeError`
132
+ otherwise). The data is zero-padded to a 4-byte boundary around that word:
133
+ the length word is the last word for `:length_word_suffix` and the first
134
+ word for `:length_word_prefix`. Because of XXTEA's 2-word minimum, empty
135
+ input produces an 8-byte ciphertext in either scheme.
136
+
126
137
  ```ruby
127
138
  XXTEA.decrypt_hex(XXTEA.encrypt_hex("", key), key) # => ""
128
139
  XXTEA.decrypt_hex(XXTEA.encrypt_hex(" ", key), key) # => " "
129
140
 
130
- XXTEA.encrypt("12345678", key).bytesize # => 12 (:pkcs7_4_min8)
131
- XXTEA.encrypt("12345678", key, padding: :pkcs7_8).bytesize # => 16 (:pkcs7_8)
141
+ XXTEA.encrypt("12345678", key).bytesize # => 12 (:pkcs7_4_min8)
142
+ XXTEA.encrypt("12345678", key, padding: :pkcs7_8).bytesize # => 16 (:pkcs7_8)
143
+ XXTEA.encrypt("12345678", key, padding: :length_word_suffix).bytesize # => 12
144
+ XXTEA.encrypt("12345678", key, padding: :length_word_prefix).bytesize # => 12
132
145
  ```
133
146
 
134
147
  You can disable padding by setting `padding: false`.
@@ -209,7 +222,7 @@ XXTEA.new("k" * 16, rounds: 2**32)
209
222
 
210
223
  ## Compatibility
211
224
 
212
- - Compatible with [Python xxtea](https://github.com/ifduyue/xxtea) (default `:pkcs7_4_min8` padding, endianness, and rounds).
225
+ - Compatible with [Python xxtea](https://github.com/ifduyue/xxtea) 6.2.0: all five padding schemes (`:pkcs7_4_min8`, `:pkcs7_8`, `:length_word_prefix`, `:length_word_suffix`, `:none`), endianness, and rounds.
213
226
  - `padding: :pkcs7_8` is compatible with [Python xxteang](https://github.com/ifduyue/xxteang).
214
227
  - The `XXTEA.encrypt(data, key)` / `XXTEA.decrypt(data, key)` class methods remain compatible with gem 0.0.1 for valid ciphertext. `XXTEA` is now a class rather than a module, and invalid padding raises `ArgumentError` instead of returning stripped bytes.
215
228
 
data/ext/xxtea/xxtea.c CHANGED
@@ -26,7 +26,9 @@
26
26
  * (https://github.com/ifduyue/xxtea): little-endian 32-bit words and
27
27
  * :pkcs7_4_min8 padding (4-byte PKCS#7-like, pad+4 for inputs shorter than 4 bytes).
28
28
  * padding: :pkcs7_8 uses 8-byte PKCS#7, compatible with Python xxteang
29
- * (https://github.com/ifduyue/xxteang).
29
+ * (https://github.com/ifduyue/xxteang); :length_word_prefix and
30
+ * :length_word_suffix prepend/append a little-endian uint32 length word
31
+ * with zero padding, matching Python xxtea 6.2.0.
30
32
  */
31
33
 
32
34
  #include "ruby.h"
@@ -50,6 +52,8 @@ static ID id_rounds;
50
52
  static ID id_none;
51
53
  static ID id_pkcs7_4_min8;
52
54
  static ID id_pkcs7_8;
55
+ static ID id_length_word_prefix;
56
+ static ID id_length_word_suffix;
53
57
  static VALUE cXXTEA;
54
58
 
55
59
  typedef struct {
@@ -127,20 +131,47 @@ bytes2longs(const char *in, long inlen, uint32_t *out, int padding)
127
131
  long i, nwords;
128
132
  int pad;
129
133
  const unsigned char *s = (const unsigned char *)in;
134
+ uint32_t *dst = out;
135
+
136
+ if (padding == 2) {
137
+ /* uint32 store, not memcpy(&inlen): 64-bit would copy the high half. */
138
+ out[0] = (uint32_t)inlen;
139
+ dst = out + 1;
140
+ }
130
141
 
131
142
  nwords = inlen >> 2;
132
143
  for (i = 0; i < nwords; i++) {
133
144
  #if XXTEA_LITTLE_ENDIAN
134
- memcpy(&out[i], s + 4 * i, 4);
145
+ memcpy(&dst[i], s + 4 * i, 4);
135
146
  #else
136
147
  const unsigned char *p = s + 4 * i;
137
- out[i] = (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
148
+ dst[i] = (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
138
149
  ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
139
150
  #endif
140
151
  }
141
152
 
142
153
  i = nwords << 2;
143
154
 
155
+ if (padding == 1 || padding == 2) {
156
+ /* XXTEA needs two words, so empty input gets an extra zero word. */
157
+ uint32_t w = 0;
158
+ int shift = 0;
159
+ for (; i < inlen; i++, shift += 8) {
160
+ w |= (uint32_t)s[i] << shift;
161
+ }
162
+ if ((inlen & 3) != 0) {
163
+ dst[nwords] = w;
164
+ nwords++;
165
+ }
166
+ if (nwords < 1) {
167
+ dst[nwords++] = 0;
168
+ }
169
+ if (padding == 1) {
170
+ dst[nwords] = (uint32_t)inlen;
171
+ }
172
+ return;
173
+ }
174
+
144
175
  if (padding == 8) {
145
176
  /*
146
177
  * 8-byte PKCS#7 (xxteang): pad = 8 - (len & 7), range 1-8.
@@ -216,8 +247,41 @@ longs2bytes(const uint32_t *in, long inlen, char *out, int padding)
216
247
 
217
248
  outlen = inlen * 4;
218
249
 
250
+ if (padding == 1 || padding == 2) {
251
+ int prefix = padding == 2;
252
+ long n = outlen - 4;
253
+ long leftover, pad_from, pad_to;
254
+ uint32_t m32;
255
+ if (prefix) {
256
+ m32 = (uint32_t)s[0] | ((uint32_t)s[1] << 8) |
257
+ ((uint32_t)s[2] << 16) | ((uint32_t)s[3] << 24);
258
+ }
259
+ else {
260
+ m32 = (uint32_t)s[n] | ((uint32_t)s[n + 1] << 8) |
261
+ ((uint32_t)s[n + 2] << 16) | ((uint32_t)s[n + 3] << 24);
262
+ }
263
+ if ((size_t)m32 > (size_t)n) {
264
+ return -1;
265
+ }
266
+ leftover = n - (long)m32;
267
+ pad_from = prefix ? 4 + (long)m32 : (long)m32;
268
+ pad_to = prefix ? outlen : n;
269
+ /* leftover 4 is the empty 2-word case; otherwise 0-3 zero-pad bytes. */
270
+ if (leftover > 3 && !(m32 == 0 && leftover == 4)) {
271
+ return -1;
272
+ }
273
+ for (i = pad_from; i < pad_to; i++) {
274
+ if (s[i] != 0) {
275
+ return -1;
276
+ }
277
+ }
278
+ if (prefix && m32 != 0) {
279
+ memmove(s, s + 4, (size_t)m32);
280
+ }
281
+ outlen = (long)m32;
282
+ }
219
283
  /* PKCS#7-style unpadding (4-byte or 8-byte; pad values 1-8). */
220
- if (padding) {
284
+ else if (padding) {
221
285
  pad = s[outlen - 1];
222
286
  outlen -= pad;
223
287
 
@@ -292,7 +356,8 @@ parse_rounds(VALUE obj)
292
356
  return NUM2UINT(obj);
293
357
  }
294
358
 
295
- /* 0 = none, 4 = :pkcs7_4_min8 (default), 8 = :pkcs7_8. */
359
+ /* 0 = none, 1 = :length_word_suffix, 2 = :length_word_prefix,
360
+ * 4 = :pkcs7_4_min8 (default), 8 = :pkcs7_8. */
296
361
  static int
297
362
  parse_padding(VALUE obj)
298
363
  {
@@ -309,12 +374,19 @@ parse_padding(VALUE obj)
309
374
  }
310
375
  if (!SYMBOL_P(obj)) {
311
376
  rb_raise(rb_eTypeError,
312
- "padding must be true, false, :none, :pkcs7_4_min8, or :pkcs7_8");
377
+ "padding must be true, false, :none, :pkcs7_4_min8, "
378
+ ":pkcs7_8, :length_word_prefix, or :length_word_suffix");
313
379
  }
314
380
  id = SYM2ID(obj);
315
381
  if (id == id_none) {
316
382
  return 0;
317
383
  }
384
+ if (id == id_length_word_suffix) {
385
+ return 1;
386
+ }
387
+ if (id == id_length_word_prefix) {
388
+ return 2;
389
+ }
318
390
  if (id == id_pkcs7_4_min8) {
319
391
  return 4;
320
392
  }
@@ -322,7 +394,8 @@ parse_padding(VALUE obj)
322
394
  return 8;
323
395
  }
324
396
  rb_raise(rb_eArgError,
325
- "unknown padding %+"PRIsVALUE" (expected :none, :pkcs7_4_min8, or :pkcs7_8)",
397
+ "unknown padding %+"PRIsVALUE" (expected :none, :pkcs7_4_min8, "
398
+ ":pkcs7_8, :length_word_prefix, or :length_word_suffix)",
326
399
  obj);
327
400
  }
328
401
 
@@ -384,6 +457,21 @@ encrypt_impl(VALUE data, const char key[16], int padding, unsigned int rounds)
384
457
  else if (padding == 4) {
385
458
  alen = data_len < 4 ? 2 : (data_len >> 2) + 1;
386
459
  }
460
+ else if (padding == 1 || padding == 2) {
461
+ /* The length word is a uint32, so the plaintext length must fit. */
462
+ #if LONG_MAX > 2147483647L
463
+ if (data_len > 0xFFFFFFFFL) {
464
+ rb_raise(rb_eRangeError, "data too large");
465
+ }
466
+ #endif
467
+ if (data_len > LONG_MAX - 4) {
468
+ rb_raise(rb_eRangeError, "data too large");
469
+ }
470
+ alen = (data_len >> 2) + ((data_len & 3) != 0 ? 1 : 0) + 1;
471
+ if (alen < 2) {
472
+ alen = 2;
473
+ }
474
+ }
387
475
  else {
388
476
  alen = data_len >> 2;
389
477
  }
@@ -628,6 +716,8 @@ xxtea_inspect(VALUE self)
628
716
  return rb_sprintf("#<%s:%p padding=%s rounds=%u>",
629
717
  rb_obj_classname(self), (void *)self,
630
718
  cipher->padding == 0 ? "false" :
719
+ cipher->padding == 1 ? "length_word_suffix" :
720
+ cipher->padding == 2 ? "length_word_prefix" :
631
721
  cipher->padding == 4 ? "pkcs7_4_min8" : "pkcs7_8",
632
722
  cipher->rounds);
633
723
  }
@@ -642,6 +732,8 @@ Init_xxtea(void)
642
732
  id_none = rb_intern("none");
643
733
  id_pkcs7_4_min8 = rb_intern("pkcs7_4_min8");
644
734
  id_pkcs7_8 = rb_intern("pkcs7_8");
735
+ id_length_word_prefix = rb_intern("length_word_prefix");
736
+ id_length_word_suffix = rb_intern("length_word_suffix");
645
737
 
646
738
  cXXTEA = rb_define_class("XXTEA", rb_cObject);
647
739
  rb_define_alloc_func(cXXTEA, xxtea_alloc);
@@ -660,4 +752,6 @@ Init_xxtea(void)
660
752
 
661
753
  rb_define_const(cXXTEA, "PKCS7_4_MIN8", ID2SYM(id_pkcs7_4_min8));
662
754
  rb_define_const(cXXTEA, "PKCS7_8", ID2SYM(id_pkcs7_8));
755
+ rb_define_const(cXXTEA, "LENGTH_WORD_PREFIX", ID2SYM(id_length_word_prefix));
756
+ rb_define_const(cXXTEA, "LENGTH_WORD_SUFFIX", ID2SYM(id_length_word_suffix));
663
757
  }
data/lib/xxtea/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class XXTEA
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xxtea
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yue Du
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-20 00:00:00.000000000 Z
11
+ date: 2026-08-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  XXTEA implemented as a Ruby C extension. Ciphertext is compatible with the