Skip to content

Commit c0124a4

Browse files
committed
Added codec: modhex
1 parent 4c8744f commit c0124a4

5 files changed

Lines changed: 73 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ CACTCGGTCGGCCATATGTTCGGCCATATGTTCGTCTGTTCACTCGCCCATACACT
277277
- [X] `base10`: simple conversion to decimal
278278
- [X] `base11`: conversion to digits with a "*a*"
279279
- [X] `base16`: simple conversion to hexadecimal (with a variant holding an alphabet with digits and letters inverted)
280+
- [X] `modhex`: [ModHex](https://developers.yubico.com/yubico-c/Manuals/modhex.1.html), the hexadecimal of YubiKeys (digits as `cbdefghijklnrtuv`)
280281
- [X] `base26`: conversion to alphabet letters
281282
- [X] `base32`: classical conversion according to the RFC4648 with all its variants ([zbase32](https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt), extended hexadecimal, [geohash](https://en.wikipedia.org/wiki/Geohash), [Crockford](https://www.crockford.com/base32))
282283
- [X] `base36`: [Base36](https://en.wikipedia.org/wiki/Base36) conversion to letters and digits (with a variant inverting both groups)

‎docs/pages/enc/base.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ This encoding implements various different versions of Base85.
183183

184184
**Codec** | **Conversions** | **Aliases** | **Comment**
185185
:---: | :---: | --- | ---
186+
`modhex` | data <-> ModHex encoded text | `mod-hex`, `base16-modhex`, `yubikey-modhex` | [ModHex](https://developers.yubico.com/yubico-c/Manuals/modhex.1.html), the hexadecimal of YubiKeys with the digits `cbdefghijklnrtuv` ; decoding ignores the case, whitespaces and the delimiters `%`, `,`, `;` and `:`
186187
`base100` | text <-> Base100 encoded text | `base[-_]?100|emoji` | Python 3 only
187188
`base122` | text <-> Base122 encoded text | `base[-_]?122` | Python 3 only
188189
`base128` | text <-> Base128 encoded text | `base[-_]?128` | Relies on the ASCII charset
@@ -198,6 +199,13 @@ This encoding implements various different versions of Base85.
198199
`base65536` | data <-> Base65536 encoded text | `base[-_]?65536` | 16 bits per character, optimised for UTF-32
199200
`base131072` | data <-> Base131072 encoded text | `base[-_]?131072` | 17 bits per character, variant of the [base131072](https://docs.rs/base131072) Rust crate
200201

202+
```python
203+
>>> codecs.encode("this is a test", "modhex")
204+
'ifhjhkiedchkiedchbdcifhgieif'
205+
>>> codecs.decode("IF:HJ:HK:IE", "modhex")
206+
'this'
207+
```
208+
201209
```python
202210
>>> codecs.encode("this is a test", "base100")
203211
'👫👟👠👪🐗👠👪🐗👘🐗👫👜👪👫'

‎src/codext/base/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .base131072 import *
2020
from .baseN import *
2121
from .leb128 import *
22+
from .modhex import *
2223
from ..__common__ import *
2324
from ..__info__ import __version__
2425

‎src/codext/base/modhex.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# -*- coding: UTF-8 -*-
2+
"""ModHex Codec - modified hexadecimal content encoding.
3+
4+
ModHex (modified hexadecimal) is the hexadecimal encoding used by YubiKeys for their one-time passwords: the digits 0 to
5+
f are replaced by the letters "cbdefghijklnrtuv", which are at the same positions on most keyboard layouts, as a
6+
YubiKey types its output as a USB keyboard. Encoding gives the digits without delimiter (as typed by a YubiKey), while
7+
decoding ignores the case, whitespaces and the delimiters "%", ",", ";" and ":" (as output by CyberChef).
8+
9+
This codec:
10+
- en/decodes strings from str to str
11+
- en/decodes strings from bytes to bytes
12+
- decodes file content to str (read)
13+
- encodes file content from str to bytes (write)
14+
15+
Reference: https://developers.yubico.com/yubico-c/Manuals/modhex.1.html
16+
"""
17+
from ..__common__ import *
18+
19+
20+
__examples__ = {
21+
'enc(modhex|mod-hex|mod_hex|base16-modhex|yubikey-modhex)': {'\n\x14\x1e': "clbfbu", 'test': "ifhgieif", '': ""},
22+
'dec(modhex)': {'clbfbu': "\n\x14\x1e", 'cl bf bu': "\n\x14\x1e", 'CL:BF:BU': "\n\x14\x1e", 'cl\r\nbf': "\n\x14",
23+
'cla': None, 'clb': None, 'clxx': None},
24+
'enc-dec(modhex)': ["test", "This is a test", "@random{512,1024,2048}"],
25+
}
26+
__guess__ = ["modhex"]
27+
28+
29+
MODHEX, HEX = "cbdefghijklnrtuv", "0123456789abcdef"
30+
_ENC, _DEC = str.maketrans(HEX, MODHEX), {c: h for c, h in zip(MODHEX, HEX)}
31+
DELIMITERS = " \t\r\n%,;:"
32+
33+
34+
def modhex_encode(text, errors="strict"):
35+
t = b(text)
36+
return t.hex().translate(_ENC), len(t)
37+
38+
39+
def modhex_decode(text, errors="strict"):
40+
_h, digits = handle_error("modhex", errors, decode=True), []
41+
for i, c in enumerate(ensure_str(text).lower()):
42+
if c in _DEC:
43+
digits.append(_DEC[c])
44+
elif c not in DELIMITERS:
45+
_h(c, i)
46+
# an odd number of digits leaves an incomplete byte, which is an error (or dropped if errors are not handled
47+
# strictly)
48+
if len(digits) % 2:
49+
_h(text[-1:], len(text) - 1)
50+
digits = digits[:-1]
51+
return bytes.fromhex("".join(digits)), len(text)
52+
53+
54+
add("modhex", modhex_encode, modhex_decode, r"^(?:mod[-_]?hex|base[-_]?16[-_]modhex|yubikey[-_]modhex)$",
55+
entropy=4., expansion_factor=2., len_charset=16, printables_rate=1.)

‎tests/test_base.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ def test_codec_base64(self):
240240
self.assertEqual(codecs.decode(b64, enc), STR)
241241
self.assertEqual(codecs.decode(b(b64), enc), b(STR))
242242

243+
def test_codec_modhex(self):
244+
# YubiKey's alphabet for the hexadecimal digits, the case and delimiters being ignored when decoding
245+
self.assertEqual(codecs.encode(STR, "modhex"), codecs.encode(STR, "base16").lower().translate(
246+
str.maketrans("0123456789abcdef", "cbdefghijklnrtuv")))
247+
self.assertEqual(codecs.decode(codecs.encode(STR, "modhex").upper(), "modhex"), STR)
248+
# invalid characters and an incomplete byte are dropped when errors are not handled strictly
249+
self.assertEqual(codecs.decode("cl?bfb", "modhex", "ignore"), "\n\x14")
250+
243251
def test_codec_base91(self):
244252
for b91, enc in zip([",X,<:WRT%yxth90oZB", ",N,<:MHJ%onjXzqeP1", "Jx&[jv4S3Wg>,71@Jk", "yJy^\\IDFsdc?Tof:L#"],
245253
["base91", "base91-inv", "base91-alt", "base91-alt-inv"]):

0 commit comments

Comments
 (0)