iobroker.byd 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TA2k <tombox2020@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ ![Logo](admin/byd.png)
2
+
3
+ # ioBroker.byd
4
+
5
+ [![NPM version](https://img.shields.io/npm/v/iobroker.byd.svg)](https://www.npmjs.com/package/iobroker.byd)
6
+ [![Downloads](https://img.shields.io/npm/dm/iobroker.byd.svg)](https://www.npmjs.com/package/iobroker.byd)
7
+ ![Number of Installations](https://iobroker.live/badges/byd-installed.svg)
8
+ ![Current version in stable repository](https://iobroker.live/badges/byd-stable.svg)
9
+
10
+ [![NPM](https://nodei.co/npm/iobroker.byd.png?downloads=true)](https://nodei.co/npm/iobroker.byd/)
11
+
12
+ **Tests:** ![Test and Release](https://github.com/TA2k/ioBroker.byd/workflows/Test%20and%20Release/badge.svg)
13
+
14
+ ## byd adapter for ioBroker
15
+
16
+ iobroker Adapter for BYD cars based on https://github.com/Niek/BYD-re
17
+
18
+ ## Changelog
19
+
20
+ <!--
21
+ Placeholder for the next version (at the beginning of the line):
22
+ ### **WORK IN PROGRESS**
23
+ -->
24
+
25
+ ### **WORK IN PROGRESS**
26
+
27
+ - (TA2k) initial release
28
+
29
+ ## License
30
+
31
+ MIT License
32
+
33
+ Copyright (c) 2026 TA2k <tombox2020@gmail.com>
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining a copy
36
+ of this software and associated documentation files (the "Software"), to deal
37
+ in the Software without restriction, including without limitation the rights
38
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
+ copies of the Software, and to permit persons to whom the Software is
40
+ furnished to do so, subject to the following conditions:
41
+
42
+ The above copyright notice and this permission notice shall be included in all
43
+ copies or substantial portions of the Software.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
51
+ SOFTWARE.
package/admin/byd.png ADDED
Binary file
@@ -0,0 +1,99 @@
1
+ {
2
+ "i18n": false,
3
+ "type": "panel",
4
+ "items": {
5
+ "username": {
6
+ "type": "text",
7
+ "label": {
8
+ "en": "Email",
9
+ "de": "E-Mail",
10
+ "ru": "Электронная почта",
11
+ "pt": "E-mail",
12
+ "nl": "E-mail",
13
+ "fr": "Email",
14
+ "it": "Email",
15
+ "es": "Correo electrónico",
16
+ "pl": "E-mail",
17
+ "uk": "Електронна пошта",
18
+ "zh-cn": "电子邮件"
19
+ },
20
+ "newLine": true,
21
+ "sm": 6
22
+ },
23
+ "password": {
24
+ "type": "password",
25
+ "label": {
26
+ "en": "Password",
27
+ "de": "Passwort",
28
+ "ru": "Пароль",
29
+ "pt": "Senha",
30
+ "nl": "Wachtwoord",
31
+ "fr": "Mot de passe",
32
+ "it": "Password",
33
+ "es": "Contraseña",
34
+ "pl": "Hasło",
35
+ "uk": "Пароль",
36
+ "zh-cn": "密码"
37
+ },
38
+ "newLine": true,
39
+ "repeat": false,
40
+ "visible": true,
41
+ "sm": 6
42
+ },
43
+ "countryCode": {
44
+ "type": "text",
45
+ "label": {
46
+ "en": "Country Code (e.g. NL, DE)",
47
+ "de": "Ländercode (z.B. NL, DE)",
48
+ "ru": "Код страны (напр. NL, DE)",
49
+ "pt": "Código do país (ex. NL, DE)",
50
+ "nl": "Landcode (bijv. NL, DE)",
51
+ "fr": "Code pays (ex. NL, DE)",
52
+ "it": "Codice paese (es. NL, DE)",
53
+ "es": "Código de país (ej. NL, DE)",
54
+ "pl": "Kod kraju (np. NL, DE)",
55
+ "uk": "Код країни (напр. NL, DE)",
56
+ "zh-cn": "国家代码(例如 NL, DE)"
57
+ },
58
+ "newLine": true,
59
+ "sm": 3
60
+ },
61
+ "language": {
62
+ "type": "text",
63
+ "label": {
64
+ "en": "Language (e.g. en, de)",
65
+ "de": "Sprache (z.B. en, de)",
66
+ "ru": "Язык (напр. en, de)",
67
+ "pt": "Idioma (ex. en, de)",
68
+ "nl": "Taal (bijv. en, de)",
69
+ "fr": "Langue (ex. en, de)",
70
+ "it": "Lingua (es. en, de)",
71
+ "es": "Idioma (ej. en, de)",
72
+ "pl": "Język (np. en, de)",
73
+ "uk": "Мова (напр. en, de)",
74
+ "zh-cn": "语言(例如 en, de)"
75
+ },
76
+ "newLine": true,
77
+ "sm": 3
78
+ },
79
+ "interval": {
80
+ "type": "number",
81
+ "label": {
82
+ "en": "Update interval (seconds)",
83
+ "de": "Aktualisierungsintervall (Sekunden)",
84
+ "ru": "Интервал обновления (секунды)",
85
+ "pt": "Intervalo de atualização (segundos)",
86
+ "nl": "Update-interval (seconden)",
87
+ "fr": "Intervalle de mise à jour (secondes)",
88
+ "it": "Intervallo di aggiornamento (secondi)",
89
+ "es": "Intervalo de actualización (segundos)",
90
+ "pl": "Interwał aktualizacji (sekundy)",
91
+ "uk": "Інтервал оновлення (секунди)",
92
+ "zh-cn": "更新间隔(秒)"
93
+ },
94
+ "newLine": true,
95
+ "min": 60,
96
+ "sm": 3
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,111 @@
1
+ {
2
+ "common": {
3
+ "name": "byd",
4
+ "version": "0.0.1",
5
+ "news": {
6
+ "0.0.1": {
7
+ "en": "initial release",
8
+ "de": "Erstveröffentlichung",
9
+ "ru": "Начальная версия",
10
+ "pt": "lançamento inicial",
11
+ "nl": "Eerste uitgave",
12
+ "fr": "Première version",
13
+ "it": "Versione iniziale",
14
+ "es": "Versión inicial",
15
+ "pl": "Pierwsze wydanie",
16
+ "uk": "Початкова версія",
17
+ "zh-cn": "首次出版"
18
+ }
19
+ },
20
+ "titleLang": {
21
+ "en": "BYD",
22
+ "de": "BYD",
23
+ "ru": "БИД",
24
+ "pt": "BYD",
25
+ "nl": "DOORD",
26
+ "fr": "BYD",
27
+ "it": "BYD",
28
+ "es": "BYD",
29
+ "pl": "BYD",
30
+ "uk": "BYD",
31
+ "zh-cn": "比亚迪"
32
+ },
33
+ "desc": {
34
+ "en": "iobroker Adapter for BYD cars",
35
+ "de": "iobroker Adapter für BYD-Autos",
36
+ "ru": "Адаптер iobroker для автомобилей BYD",
37
+ "pt": "Adaptador iobroker para carros BYD",
38
+ "nl": "iobroker Adapter voor BYD-auto's",
39
+ "fr": "Adaptateur iobroker pour voitures BYD",
40
+ "it": "Adattatore iobroker per auto BYD",
41
+ "es": "Adaptador iobroker para coches BYD",
42
+ "pl": "iobroker Adapter do samochodów BYD",
43
+ "uk": "iobroker Адаптер для автомобілів BYD",
44
+ "zh-cn": "比亚迪汽车iobroker适配器"
45
+ },
46
+ "authors": ["TA2k <tombox2020@gmail.com>"],
47
+ "keywords": ["byd", "car"],
48
+ "licenseInformation": {
49
+ "type": "free",
50
+ "license": "MIT"
51
+ },
52
+ "platform": "Javascript/Node.js",
53
+ "icon": "byd.png",
54
+ "enabled": true,
55
+ "extIcon": "https://raw.githubusercontent.com/TA2k/ioBroker.byd/main/admin/byd.png",
56
+ "readme": "https://github.com/TA2k/ioBroker.byd/blob/main/README.md",
57
+ "loglevel": "info",
58
+ "tier": 3,
59
+ "mode": "daemon",
60
+ "type": "vehicle",
61
+ "compact": true,
62
+ "connectionType": "cloud",
63
+ "dataSource": "poll",
64
+ "adminUI": {
65
+ "config": "json"
66
+ },
67
+ "dependencies": [
68
+ {
69
+ "js-controller": ">=6.0.11"
70
+ }
71
+ ],
72
+ "globalDependencies": [
73
+ {
74
+ "admin": ">=7.0.23"
75
+ }
76
+ ]
77
+ },
78
+ "native": {
79
+ "username": "",
80
+ "password": "",
81
+ "countryCode": "NL",
82
+ "language": "en",
83
+ "interval": 300
84
+ },
85
+ "encryptedNative": ["password"],
86
+ "protectedNative": ["password"],
87
+ "objects": [],
88
+ "instanceObjects": [
89
+ {
90
+ "_id": "info",
91
+ "type": "channel",
92
+ "common": {
93
+ "name": "Information"
94
+ },
95
+ "native": {}
96
+ },
97
+ {
98
+ "_id": "info.connection",
99
+ "type": "state",
100
+ "common": {
101
+ "role": "indicator.connected",
102
+ "name": "Device or service connected",
103
+ "type": "boolean",
104
+ "read": true,
105
+ "write": false,
106
+ "def": false
107
+ },
108
+ "native": {}
109
+ }
110
+ ]
111
+ }
@@ -0,0 +1,19 @@
1
+ // This file extends the AdapterConfig type from "@iobroker/types"
2
+ // using the actual properties present in io-package.json
3
+ // in order to provide typings for adapter.config properties
4
+
5
+ import { native } from '../io-package.json';
6
+
7
+ type _AdapterConfig = typeof native;
8
+
9
+ // Augment the globally declared type ioBroker.AdapterConfig
10
+ declare global {
11
+ namespace ioBroker {
12
+ interface AdapterConfig extends _AdapterConfig {
13
+ // Do not enter anything here!
14
+ }
15
+ }
16
+ }
17
+
18
+ // this is required so the above AdapterConfig is found by TypeScript / type checking
19
+ export {};
package/lib/bangcle.js ADDED
@@ -0,0 +1,379 @@
1
+ 'use strict';
2
+
3
+ // Generated from byd/libencrypt.so.mem.so via scripts/generate_bangcle_auth_tables.js.
4
+ const encodedAuthTables = require('./bangcle_auth_tables');
5
+
6
+ function decodeTable(name, expectedLength) {
7
+ const base64 = encodedAuthTables[name];
8
+ if (typeof base64 !== 'string' || !base64.length) {
9
+ throw new Error(`Missing embedded auth table: ${name}`);
10
+ }
11
+ const table = Buffer.from(base64, 'base64');
12
+ if (table.length !== expectedLength) {
13
+ throw new Error(`Embedded auth table ${name} has unexpected size ${table.length} (expected ${expectedLength})`);
14
+ }
15
+ return table;
16
+ }
17
+
18
+ const AUTH_TABLES = Object.freeze({
19
+ invRound: decodeTable('invRound', 0x28000),
20
+ invXor: decodeTable('invXor', 0x3c000),
21
+ invFirst: decodeTable('invFirst', 0x1000),
22
+ round: decodeTable('round', 0x28000),
23
+ xor: decodeTable('xor', 0x3c000),
24
+ final: decodeTable('final', 0x1000),
25
+ permDecrypt: decodeTable('permDecrypt', 8),
26
+ permEncrypt: decodeTable('permEncrypt', 8),
27
+ });
28
+
29
+ function prepareAESMatrix(input, output) {
30
+ for (let col = 0; col < 4; col += 1) {
31
+ for (let row = 0; row < 4; row += 1) {
32
+ output[col * 8 + row] = input[col + row * 4];
33
+ }
34
+ }
35
+ }
36
+
37
+ function decryptBlockAuth(block, round = 1, scratch = {}) {
38
+ const tables = AUTH_TABLES;
39
+ const param3 = typeof round === 'number' && Number.isFinite(round) ? round : 1;
40
+
41
+ const state = scratch.state32 || new Uint8Array(32);
42
+ scratch.state32 = state;
43
+ const temp64 = scratch.temp64 || Buffer.alloc(64);
44
+ scratch.temp64 = temp64;
45
+ const tmp32 = scratch.tmp32 || new Uint8Array(32);
46
+ scratch.tmp32 = tmp32;
47
+ const output = scratch.out || new Uint8Array(16);
48
+ scratch.out = output;
49
+
50
+ prepareAESMatrix(block, state);
51
+
52
+ for (let round = 9; round >= Math.max(1, param3); round -= 1) {
53
+ const lVar20 = round;
54
+ const lVar21 = lVar20 * 4;
55
+ let permPtr = 0;
56
+
57
+ for (let i = 0; i < 4; i += 1) {
58
+ const bVar3 = tables.permDecrypt[permPtr];
59
+ const lVar9 = i;
60
+ const lVar16 = lVar9 * 8;
61
+ const base = i * 16;
62
+
63
+ for (let j = 0; j < 4; j += 1) {
64
+ const uVar7 = (bVar3 + j) & 3;
65
+ const byteVal = state[lVar16 + uVar7];
66
+ const idx = byteVal + (lVar9 + (lVar21 + uVar7) * 4) * 256;
67
+ const value = tables.invRound.readUInt32LE(idx * 4);
68
+ temp64.writeUInt32LE(value, base + j * 4);
69
+ }
70
+ permPtr += 2;
71
+ }
72
+
73
+ let iVar15 = 1;
74
+ for (let lVar21Xor = 0; lVar21Xor < 4; lVar21Xor += 1) {
75
+ let pbVar18Offset = lVar21Xor;
76
+
77
+ for (let lVar9Xor = 0; lVar9Xor < 4; lVar9Xor += 1) {
78
+ const local10 = temp64[pbVar18Offset];
79
+ let uVar6 = local10 & 0xf;
80
+ let uVar26 = local10 & 0xf0;
81
+
82
+ const localF0 = temp64[pbVar18Offset + 0x10];
83
+ const localF1 = temp64[pbVar18Offset + 0x20];
84
+ const localF2 = temp64[pbVar18Offset + 0x30];
85
+
86
+ const lVar2 = lVar9Xor * 0x18 + lVar20 * 0x60;
87
+ let iVar25 = iVar15;
88
+
89
+ for (let lVar16 = 0; lVar16 < 3; lVar16 += 1) {
90
+ const bVar3 = lVar16 === 0 ? localF0 : lVar16 === 1 ? localF1 : localF2;
91
+ const uVar1 = (bVar3 << 4) & 0xff;
92
+ const uVar27 = uVar6 | uVar1;
93
+ uVar26 = (uVar26 >> 4) | ((bVar3 >> 4) << 4);
94
+
95
+ const idx1 = (lVar2 + (iVar25 - 1)) * 0x100 + uVar27;
96
+ uVar6 = tables.invXor[idx1] & 0xf;
97
+
98
+ const idx2 = (lVar2 + iVar25) * 0x100 + uVar26;
99
+ const bVar3New = tables.invXor[idx2];
100
+ uVar26 = (bVar3New & 0xf) << 4;
101
+ iVar25 += 2;
102
+ }
103
+
104
+ state[lVar9Xor + lVar21Xor * 8] = (uVar26 | uVar6) & 0xff;
105
+ pbVar18Offset += 4;
106
+ }
107
+ iVar15 += 6;
108
+ }
109
+ }
110
+
111
+ if (param3 === 1) {
112
+ tmp32.set(state);
113
+ let uVar8 = 1;
114
+ let uVar10 = 3;
115
+ let uVar12 = 2;
116
+
117
+ for (let row = 0; row < 4; row += 1) {
118
+ const idx0 = tmp32[row] + row * 0x400;
119
+ state[row] = tables.invFirst[idx0];
120
+
121
+ const row1 = uVar10 & 3;
122
+ const idx1 = tmp32[8 + row1] + row1 * 0x400 + 0x100;
123
+ state[8 + row] = tables.invFirst[idx1];
124
+
125
+ const row2 = uVar12 & 3;
126
+ const idx2 = tmp32[0x10 + row2] + row2 * 0x400 + 0x200;
127
+ state[0x10 + row] = tables.invFirst[idx2];
128
+
129
+ const row3 = uVar8 & 3;
130
+ const idx3 = tmp32[0x18 + row3] + row3 * 0x400 + 0x300;
131
+ state[0x18 + row] = tables.invFirst[idx3];
132
+
133
+ uVar8 += 1;
134
+ uVar10 += 1;
135
+ uVar12 += 1;
136
+ }
137
+ }
138
+
139
+ for (let col = 0; col < 4; col += 1) {
140
+ for (let row = 0; row < 4; row += 1) {
141
+ output[col + row * 4] = state[col * 8 + row];
142
+ }
143
+ }
144
+ return output;
145
+ }
146
+
147
+ function encryptBlockAuth(block, round = 10, scratch = {}) {
148
+ const tables = AUTH_TABLES;
149
+ const param3 = typeof round === 'number' && Number.isFinite(round) ? round : 10;
150
+
151
+ const state = scratch.state32 || new Uint8Array(32);
152
+ scratch.state32 = state;
153
+ const temp64 = scratch.temp64 || Buffer.alloc(64);
154
+ scratch.temp64 = temp64;
155
+ const tmp32 = scratch.tmp32 || new Uint8Array(32);
156
+ scratch.tmp32 = tmp32;
157
+ const output = scratch.out || new Uint8Array(16);
158
+ scratch.out = output;
159
+
160
+ prepareAESMatrix(block, state);
161
+
162
+ const rounds = Math.min(9, Math.max(0, param3));
163
+ for (let round = 0; round < rounds; round += 1) {
164
+ const lVar21 = round * 4;
165
+ let permPtr = 0;
166
+
167
+ for (let i = 0; i < 4; i += 1) {
168
+ const bVar4 = tables.permEncrypt[permPtr];
169
+ const lVar9 = i;
170
+ const lVar16 = lVar9 * 8;
171
+ const base = i * 16;
172
+
173
+ for (let j = 0; j < 4; j += 1) {
174
+ const uVar8 = (bVar4 + j) & 3;
175
+ const byteVal = state[lVar16 + uVar8];
176
+ const idx = byteVal + (lVar9 + (lVar21 + uVar8) * 4) * 256;
177
+ const value = tables.round.readUInt32LE(idx * 4);
178
+ temp64.writeUInt32LE(value, base + j * 4);
179
+ }
180
+ permPtr += 2;
181
+ }
182
+
183
+ let iVar16 = 1;
184
+ for (let lVar22 = 0; lVar22 < 4; lVar22 += 1) {
185
+ let pbVar19Offset = lVar22;
186
+ for (let lVar10 = 0; lVar10 < 4; lVar10 += 1) {
187
+ const local10 = temp64[pbVar19Offset];
188
+ let uVar7 = local10 & 0xf;
189
+ let uVar26 = local10 & 0xf0;
190
+
191
+ const localF0 = temp64[pbVar19Offset + 0x10];
192
+ const localF1 = temp64[pbVar19Offset + 0x20];
193
+ const localF2 = temp64[pbVar19Offset + 0x30];
194
+
195
+ const lVar2 = lVar10 * 0x18 + round * 0x60;
196
+ let iVar25 = iVar16;
197
+
198
+ for (let lVar17 = 0; lVar17 < 3; lVar17 += 1) {
199
+ const bVar4 = lVar17 === 0 ? localF0 : lVar17 === 1 ? localF1 : localF2;
200
+ const uVar1 = (bVar4 << 4) & 0xff;
201
+ const uVar27 = uVar7 | uVar1;
202
+ uVar26 = (uVar26 >> 4) | ((bVar4 >> 4) << 4);
203
+
204
+ const idx1 = (lVar2 + (iVar25 - 1)) * 0x100 + uVar27;
205
+ uVar7 = tables.xor[idx1] & 0xf;
206
+
207
+ const idx2 = (lVar2 + iVar25) * 0x100 + uVar26;
208
+ const bVar4New = tables.xor[idx2];
209
+ uVar26 = (bVar4New & 0xf) << 4;
210
+ iVar25 += 2;
211
+ }
212
+
213
+ state[lVar10 + lVar22 * 8] = (uVar26 | uVar7) & 0xff;
214
+ pbVar19Offset += 4;
215
+ }
216
+ iVar16 += 6;
217
+ }
218
+ }
219
+
220
+ if (param3 === 10) {
221
+ tmp32.set(state);
222
+ let uVar13 = 3;
223
+ let uVar9 = 2;
224
+ let uVar11 = 1;
225
+ let uVar8 = 0;
226
+
227
+ for (let row = 0; row < 4; row += 1) {
228
+ const row0 = uVar8 & 3;
229
+ state[row] = tables.final[tmp32[row0] + row0 * 0x400];
230
+
231
+ const row1 = uVar11 & 3;
232
+ state[8 + row] = tables.final[tmp32[8 + row1] + row1 * 0x400 + 0x100];
233
+
234
+ const row2 = uVar9 & 3;
235
+ state[0x10 + row] = tables.final[tmp32[0x10 + row2] + row2 * 0x400 + 0x200];
236
+
237
+ const row3 = uVar13 & 3;
238
+ state[0x18 + row] = tables.final[tmp32[0x18 + row3] + row3 * 0x400 + 0x300];
239
+
240
+ uVar8 += 1;
241
+ uVar11 += 1;
242
+ uVar9 += 1;
243
+ uVar13 += 1;
244
+ }
245
+ }
246
+
247
+ for (let col = 0; col < 4; col += 1) {
248
+ for (let row = 0; row < 4; row += 1) {
249
+ output[col + row * 4] = state[col * 8 + row];
250
+ }
251
+ }
252
+ return output;
253
+ }
254
+
255
+ function xorInto(target, source) {
256
+ for (let i = 0; i < target.length; i += 1) {
257
+ target[i] ^= source[i];
258
+ }
259
+ }
260
+
261
+ function decryptCbc(data, iv) {
262
+ if (data.length % 16 !== 0) {
263
+ throw new Error('Bangcle ciphertext length must be multiple of 16');
264
+ }
265
+ if (iv.length !== 16) {
266
+ throw new Error('Bangcle CBC IV must be 16 bytes');
267
+ }
268
+ const scratch = {
269
+ state32: new Uint8Array(32),
270
+ tmp32: new Uint8Array(32),
271
+ temp64: Buffer.alloc(64),
272
+ out: new Uint8Array(16),
273
+ };
274
+ const result = Buffer.alloc(data.length);
275
+ let prev = Uint8Array.from(iv);
276
+
277
+ for (let offset = 0; offset < data.length; offset += 16) {
278
+ const block = data.subarray(offset, offset + 16);
279
+ const decrypted = decryptBlockAuth(block, 1, scratch);
280
+ const decoded = Buffer.from(decrypted);
281
+ xorInto(decoded, prev);
282
+ decoded.copy(result, offset);
283
+ prev = Uint8Array.from(block);
284
+ }
285
+ return result;
286
+ }
287
+
288
+ function encryptCbc(data, iv) {
289
+ if (data.length % 16 !== 0) {
290
+ throw new Error('Bangcle plaintext length must be multiple of 16');
291
+ }
292
+ if (iv.length !== 16) {
293
+ throw new Error('Bangcle CBC IV must be 16 bytes');
294
+ }
295
+ const scratch = {
296
+ state32: new Uint8Array(32),
297
+ tmp32: new Uint8Array(32),
298
+ temp64: Buffer.alloc(64),
299
+ out: new Uint8Array(16),
300
+ };
301
+ const result = Buffer.alloc(data.length);
302
+ let prev = Uint8Array.from(iv);
303
+ for (let offset = 0; offset < data.length; offset += 16) {
304
+ const block = Buffer.from(data.subarray(offset, offset + 16));
305
+ xorInto(block, prev);
306
+ const encrypted = encryptBlockAuth(block, 10, scratch);
307
+ Buffer.from(encrypted).copy(result, offset);
308
+ prev = Uint8Array.from(encrypted);
309
+ }
310
+ return result;
311
+ }
312
+
313
+ function stripPkcs7(buffer) {
314
+ if (buffer.length === 0) {
315
+ return buffer;
316
+ }
317
+ const pad = buffer[buffer.length - 1];
318
+ if (pad === 0 || pad > 16) {
319
+ return buffer;
320
+ }
321
+ for (let i = buffer.length - pad; i < buffer.length; i += 1) {
322
+ if (buffer[i] !== pad) {
323
+ return buffer;
324
+ }
325
+ }
326
+ return buffer.slice(0, buffer.length - pad);
327
+ }
328
+
329
+ function addPkcs7(buffer, blockSize = 16) {
330
+ const remainder = buffer.length % blockSize;
331
+ const pad = remainder === 0 ? blockSize : blockSize - remainder;
332
+ return Buffer.concat([buffer, Buffer.alloc(pad, pad)]);
333
+ }
334
+
335
+ function normaliseCheckcodeInput(input) {
336
+ let cleaned = String(input || '')
337
+ .replace(/\s+/g, '')
338
+ .trim();
339
+ cleaned = cleaned.replace(/-/g, '+').replace(/_/g, '/');
340
+ if (!cleaned.length) {
341
+ throw new Error('Bangcle input is empty');
342
+ }
343
+ if ((cleaned.startsWith('F') || cleaned.startsWith('S')) && cleaned.length > 1) {
344
+ cleaned = cleaned.slice(1);
345
+ }
346
+ const remainder = cleaned.length % 4;
347
+ if (remainder !== 0) {
348
+ cleaned = `${cleaned}${'='.repeat(4 - remainder)}`;
349
+ }
350
+ return cleaned;
351
+ }
352
+
353
+ function decodeEnvelope(base64) {
354
+ const payload = normaliseCheckcodeInput(base64);
355
+ const ciphertext = Buffer.from(payload, 'base64');
356
+ if (!ciphertext.length) {
357
+ throw new Error('Bangcle ciphertext is empty');
358
+ }
359
+ if (ciphertext.length % 16 !== 0) {
360
+ throw new Error(`Bangcle ciphertext length ${ciphertext.length} is incompatible with 16-byte blocks`);
361
+ }
362
+
363
+ const iv = Buffer.alloc(16, 0);
364
+ const plaintext = decryptCbc(ciphertext, iv);
365
+ return stripPkcs7(plaintext);
366
+ }
367
+
368
+ function encodeEnvelope(plaintext) {
369
+ const plainBuf = Buffer.isBuffer(plaintext) ? Buffer.from(plaintext) : Buffer.from(String(plaintext), 'utf8');
370
+ const padded = addPkcs7(plainBuf);
371
+ const iv = Buffer.alloc(16, 0);
372
+ const ciphertext = encryptCbc(padded, iv);
373
+ return `F${ciphertext.toString('base64')}`;
374
+ }
375
+
376
+ module.exports = {
377
+ decodeEnvelope,
378
+ encodeEnvelope,
379
+ };