Skip to content

Commit 6e66933

Browse files
committed
chore: create abstract class for VDS/IDB
1 parent facd7cb commit 6e66933

9 files changed

Lines changed: 90 additions & 75 deletions

File tree

src/generic.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { bytesToNumberBE } from "@noble/curves/utils.js";
2+
import { concatBytes } from "@noble/hashes/utils.js";
3+
import { DerTLV } from "./utils.js";
4+
5+
/** Abstract seal */
6+
export abstract class AbstractSeal {
7+
/** Signed bytes */
8+
abstract get signedBytes(): Uint8Array;
9+
10+
/** Signature bytes */
11+
abstract get signatureBytes(): Uint8Array | null;
12+
13+
/** Encoded seal/barcode */
14+
abstract get encoded(): unknown;
15+
}
16+
17+
/** Abstract ECDSA signature (Raw format) */
18+
export abstract class AbstractECDSARawSignature {
19+
static readonly TAG: number;
20+
abstract readonly TAG: number;
21+
private _r: Uint8Array;
22+
private _s: Uint8Array;
23+
24+
constructor(r: Uint8Array, s: Uint8Array) {
25+
this._r = r;
26+
this._s = s;
27+
}
28+
29+
/** `r` as bigint */
30+
get r(): bigint { return bytesToNumberBE(this._r); }
31+
/** `r` as bytes */
32+
get rBytes(): Uint8Array { return this._r; }
33+
/** `s` as bigint */
34+
get s(): bigint { return bytesToNumberBE(this._s); }
35+
/** `s` as bytes */
36+
get sBytes(): Uint8Array { return this._s; }
37+
38+
/** Encoded IDB signature */
39+
get encoded(): Uint8Array {
40+
return new DerTLV(this.TAG, concatBytes(this.rBytes, this.sBytes)).encoded;
41+
}
42+
43+
/** Encoded IDB signature (as ASN.1) */
44+
toDER(): Uint8Array {
45+
return new DerTLV(0x30, concatBytes(
46+
DerTLV.getDerInteger(this.rBytes),
47+
DerTLV.getDerInteger(this.sBytes),
48+
)).encoded;
49+
}
50+
51+
static decode<T extends AbstractECDSARawSignature>(
52+
this: {
53+
new (r: Uint8Array, s: Uint8Array): T;
54+
readonly TAG: number;
55+
},
56+
data: Uint8Array
57+
): T {
58+
if(data[0] != this.TAG) throw new Error("Signature tag mismatch");
59+
const parsed = DerTLV.decode(data);
60+
if(parsed == null) throw new Error("Invalid signature");
61+
return new this(
62+
parsed.value.subarray(0, parsed.value.length / 2),
63+
parsed.value.subarray(parsed.value.length / 2, parsed.value.length)
64+
);
65+
}
66+
}

src/idb/idb.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { concatBytes } from "@noble/curves/utils.js";
2-
import { AbstractECDSARawSignature, C40Encoder, DateEncoder, DerTLV, parseTLVs } from "../utils.js";
2+
import { C40Encoder, DateEncoder, DerTLV, parseTLVs } from "../utils.js";
33
import { deflate, inflate } from "pako";
44
import { base32nopad } from "@scure/base";
5+
import { AbstractECDSARawSignature, AbstractSeal } from "../generic.js"
56

67
/** IDB signature algorithm */
78
export enum IDBSignatureAlgorithm {
@@ -19,9 +20,6 @@ export enum IDBSignatureAlgorithm {
1920
* Described by ICAO Datastructure for Barcode section 3.2
2021
*/
2122
export class IDBHeader {
22-
/** Offset for decoding */
23-
public _offset = 0;
24-
2523
/**
2624
* Barcode header
2725
* @param countryIdentifier Issuing country code
@@ -66,9 +64,7 @@ export class IDBHeader {
6664
const signatureCreationDate = DateEncoder.decodeMaskedDate(data.subarray(offset, offset + 4));
6765
offset += 4;
6866

69-
const decoded = new IDBHeader(countryIdentifier, signatureAlgorithm, certificateReference, signatureCreationDate);
70-
decoded._offset = offset;
71-
return decoded;
67+
return new IDBHeader(countryIdentifier, signatureAlgorithm, certificateReference, signatureCreationDate);
7268
}
7369
}
7470

@@ -162,7 +158,7 @@ export class IDBPayload {
162158
*
163159
* Described by ICAO Datastructure for Barcode section 2
164160
*/
165-
export class ICAOBarcode {
161+
export class ICAOBarcode implements AbstractSeal {
166162
/** Barcode identifier (Old) */
167163
static readonly BARCODE_IDENTIFIER_OLD = "NDB1";
168164
/** Barcode identifier */
@@ -187,11 +183,9 @@ export class ICAOBarcode {
187183
/** Is barcode compressed? */
188184
get isZipped(): boolean { return ((this._flag - 0x41) & 2) == 2; }
189185

190-
/** Signed bytes */
191186
get signedBytes(): Uint8Array {
192187
return concatBytes(this.header.encoded, this.payload.messageListEncoded);
193188
}
194-
/** Signature bytes */
195189
get signatureBytes(): Uint8Array | null {
196190
return this.payload.signature ? this.payload.signature.toDER() : null;
197191
}

src/utils.ts

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ export class C40Encoder {
126126
.replaceAll("\r", "")
127127
.replaceAll("\n", "");
128128

129-
let len = dataString.length;
130-
129+
const len = dataString.length;
131130
for(let i = 0; i < len; i++) {
132131
if (i % 3 == 0) {
133132
if (i + 2 < len) {
@@ -296,55 +295,4 @@ export const intToBytesBE = (value: bigint | number): Uint8Array => {
296295
}
297296

298297
return new Uint8Array(bytes);
299-
}
300-
301-
/** Abstract ECDSA signature (Raw format) */
302-
export abstract class AbstractECDSARawSignature {
303-
static readonly TAG: number;
304-
abstract readonly TAG: number;
305-
private _r: Uint8Array;
306-
private _s: Uint8Array;
307-
308-
constructor(r: Uint8Array, s: Uint8Array) {
309-
this._r = r;
310-
this._s = s;
311-
}
312-
313-
/** `r` as bigint */
314-
get r(): bigint { return bytesToNumberBE(this._r); }
315-
/** `r` as bytes */
316-
get rBytes(): Uint8Array { return this._r; }
317-
/** `s` as bigint */
318-
get s(): bigint { return bytesToNumberBE(this._s); }
319-
/** `s` as bytes */
320-
get sBytes(): Uint8Array { return this._s; }
321-
322-
/** Encoded IDB signature */
323-
get encoded(): Uint8Array {
324-
return new DerTLV(this.TAG, concatBytes(this.rBytes, this.sBytes)).encoded;
325-
}
326-
327-
/** Encoded IDB signature (as ASN.1) */
328-
toDER(): Uint8Array {
329-
return new DerTLV(0x30, concatBytes(
330-
DerTLV.getDerInteger(this.rBytes),
331-
DerTLV.getDerInteger(this.sBytes),
332-
)).encoded;
333-
}
334-
335-
static decode<T extends AbstractECDSARawSignature>(
336-
this: {
337-
new (r: Uint8Array, s: Uint8Array): T;
338-
readonly TAG: number;
339-
},
340-
data: Uint8Array
341-
): T {
342-
if(data[0] != this.TAG) throw new Error("Signature tag mismatch");
343-
const parsed = DerTLV.decode(data);
344-
if(parsed == null) throw new Error("Invalid signature");
345-
return new this(
346-
parsed.value.subarray(0, parsed.value.length / 2),
347-
parsed.value.subarray(parsed.value.length / 2, parsed.value.length)
348-
);
349-
}
350298
}

src/vds/vds.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { concatBytes } from "@noble/curves/utils.js";
2-
import { AbstractECDSARawSignature, C40Encoder, DateEncoder, DerTLV, parseTLVs } from "../utils.js";
2+
import { C40Encoder, DateEncoder, DerTLV, parseTLVs } from "../utils.js";
3+
import { AbstractECDSARawSignature, AbstractSeal } from "../generic.js"
34

45
/**
56
* Seal header
@@ -94,11 +95,11 @@ export class VDSHeader {
9495

9596
const magicByte = data[offset];
9697
offset += 1;
97-
if(magicByte != VDSHeader.TAG) throw new Error("Magic Constant mismatch");
98+
if(magicByte != VDSHeader.TAG) throw new Error("Magic constant mismatch");
9899

99100
const rawVersion = data[offset];
100101
offset += 1;
101-
if(!(rawVersion == 2 || rawVersion == 3)) throw new Error("Unsupported raw version");
102+
if(!(rawVersion == 2 || rawVersion == 3)) throw new Error("Unsupported VDS version");
102103

103104
const issuingCountry = C40Encoder.decode(data.subarray(offset, offset + 2));
104105
offset += 2;
@@ -109,8 +110,16 @@ export class VDSHeader {
109110
offset += 4;
110111
signerIdentifier = signerIdentifierAndCertRefLength.substring(0, 4);
111112

112-
const certRefLength = parseInt(signerIdentifierAndCertRefLength.substring(4), 16);
113-
const bytesToDecode = (Math.floor((certRefLength - 1) / 3) * 2) + 2;
113+
//const certRefLength = parseInt(signerIdentifierAndCertRefLength.substring(4), 16);
114+
//const bytesToDecode = (Math.floor((certRefLength - 1) / 3) * 2) + 2;
115+
let bytesToDecode;
116+
if(signerIdentifier == "DEZV") {
117+
const certRefLength = parseInt(signerIdentifierAndCertRefLength.substring(4));
118+
bytesToDecode = Math.floor((2 * certRefLength + 2) / 3);
119+
} else {
120+
const certRefLength = parseInt(signerIdentifierAndCertRefLength.substring(4), 16);
121+
bytesToDecode = (Math.floor((certRefLength - 1) / 3) * 2) + 2;
122+
}
114123

115124
certificateReference = C40Encoder.decode(data.subarray(offset, offset + bytesToDecode));
116125
offset += bytesToDecode;
@@ -152,7 +161,7 @@ export class VDSSignature extends AbstractECDSARawSignature {
152161
*
153162
* Described by ICAO p.13 section 2
154163
*/
155-
export class Seal {
164+
export class Seal implements AbstractSeal {
156165
/**
157166
* Visible digital seal (VDS)
158167
* @param header VDS header
@@ -165,24 +174,22 @@ export class Seal {
165174
public signature: VDSSignature | null = null
166175
) {}
167176

168-
/** Signed bytes */
169177
get signedBytes(): Uint8Array {
170178
return concatBytes(this.header.encoded, ...this.messageList.map(i => i.encoded));
171179
}
172-
/** Signature bytes */
173180
get signatureBytes(): Uint8Array | null {
174181
return this.signature ? this.signature.toDER() : null;
175182
}
176183

177-
/** Encoded visible digital seal */
184+
/** Encoded VDS */
178185
get encoded(): Uint8Array {
179186
let encoded = this.signedBytes;
180187
if(this.signature) encoded = concatBytes(encoded, this.signature.encoded);
181188

182189
return encoded;
183190
}
184191

185-
/** Decode visible digital seal from bytes */
192+
/** Decode VDS from bytes */
186193
static decode(data: Uint8Array): Seal {
187194
const header = VDSHeader.decode(data);
188195
const messageList: DerTLV[] = [];
File renamed without changes.
File renamed without changes.

tests/feature.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ class SocialInsuranceCard {
9898
@VDSProp({ tag: 3, coding: FeatureCoding.UTF8_STRING })
9999
firstName!: string;
100100

101-
@VDSProp({ tag: 4, coding: FeatureCoding.UTF8_STRING })
102-
birthName!: string;
101+
@VDSProp({ tag: 4, coding: FeatureCoding.UTF8_STRING, optional: true })
102+
birthName?: string;
103103
}
104104

105105
@VDSDocument({ documentRef: 0xfa06, version: 4 })

0 commit comments

Comments
 (0)