11package io .opentdf .platform .sdk ;
22
3- import org .bouncycastle .asn1 .ASN1EncodableVector ;
4- import org .bouncycastle .asn1 .ASN1InputStream ;
5- import org .bouncycastle .asn1 .ASN1Primitive ;
6- import org .bouncycastle .asn1 .ASN1Sequence ;
7- import org .bouncycastle .asn1 .ASN1TaggedObject ;
8- import org .bouncycastle .asn1 .DEROctetString ;
9- import org .bouncycastle .asn1 .DERSequence ;
10- import org .bouncycastle .asn1 .DERTaggedObject ;
11- import org .bouncycastle .crypto .digests .SHA256Digest ;
12- import org .bouncycastle .crypto .generators .HKDFBytesGenerator ;
13- import org .bouncycastle .crypto .params .HKDFParameters ;
14-
15- import java .io .ByteArrayInputStream ;
16- import java .io .IOException ;
173import java .security .MessageDigest ;
184import java .security .NoSuchAlgorithmException ;
195import java .util .Base64 ;
206
217/**
228 * Dispatcher and shared helpers for hybrid post-quantum key wrapping
23- * (X-Wing and NIST EC + ML-KEM). Mirrors the lib/ocrypto Go package.
9+ * (X-Wing and NIST EC + ML-KEM).
2410 *
2511 * Wire format: ASN.1 DER SEQUENCE with two IMPLICIT context-tagged OCTET STRINGs
2612 * SEQUENCE { [0] IMPLICIT OCTET STRING ciphertext, [1] IMPLICIT OCTET STRING encryptedDEK }
@@ -32,6 +18,11 @@ final class HybridCrypto {
3218
3319 static final int WRAP_KEY_SIZE = 32 ;
3420
21+ // ASN.1 tag bytes used by the envelope.
22+ private static final int TAG_SEQUENCE = 0x30 ;
23+ private static final int TAG_CONTEXT_PRIMITIVE_0 = 0x80 ;
24+ private static final int TAG_CONTEXT_PRIMITIVE_1 = 0x81 ;
25+
3526 private HybridCrypto () {}
3627
3728 /**
@@ -57,59 +48,131 @@ static byte[] wrapDEK(KeyType keyType, String publicKeyPEM, byte[] dek) {
5748 * Build the ASN.1 envelope from a hybrid KEM ciphertext and the AES-GCM(iv||ct) encrypted DEK.
5849 */
5950 static byte [] marshalEnvelope (byte [] hybridCiphertext , byte [] encryptedDEK ) {
60- ASN1EncodableVector v = new ASN1EncodableVector ();
61- v .add (new DERTaggedObject (false , 0 , new DEROctetString (hybridCiphertext )));
62- v .add (new DERTaggedObject (false , 1 , new DEROctetString (encryptedDEK )));
63- try {
64- return new DERSequence (v ).getEncoded ("DER" );
65- } catch (IOException e ) {
66- throw new SDKException ("failed to encode hybrid wrapped key envelope" , e );
67- }
51+ byte [] body = concat (
52+ encodeTLV (TAG_CONTEXT_PRIMITIVE_0 , hybridCiphertext ),
53+ encodeTLV (TAG_CONTEXT_PRIMITIVE_1 , encryptedDEK ));
54+ return encodeTLV (TAG_SEQUENCE , body );
6855 }
6956
7057 /**
7158 * Parse the ASN.1 envelope. Returns {@code [hybridCiphertext, encryptedDEK]}.
7259 * Rejects trailing bytes (matches the Go {@code asn1.Unmarshal} strict behaviour).
7360 */
7461 static byte [][] unmarshalEnvelope (byte [] der ) {
75- try (ASN1InputStream in = new ASN1InputStream (new ByteArrayInputStream (der ))) {
76- ASN1Primitive prim = in .readObject ();
77- if (prim == null ) {
78- throw new SDKException ("hybrid wrapped key envelope is empty" );
79- }
80- if (in .readObject () != null ) {
81- throw new SDKException ("hybrid wrapped key envelope has trailing bytes" );
82- }
83- ASN1Sequence seq = ASN1Sequence .getInstance (prim );
84- if (seq .size () != 2 ) {
85- throw new SDKException ("hybrid wrapped key envelope must have 2 elements, got " + seq .size ());
86- }
87- byte [] hybridCt = readImplicitOctetString (seq .getObjectAt (0 ), 0 );
88- byte [] encDek = readImplicitOctetString (seq .getObjectAt (1 ), 1 );
89- return new byte [][] { hybridCt , encDek };
90- } catch (IOException e ) {
91- throw new SDKException ("failed to decode hybrid wrapped key envelope" , e );
62+ Cursor c = new Cursor (der , 0 );
63+ int tag = c .readByte ();
64+ if (tag != TAG_SEQUENCE ) {
65+ throw new SDKException ("expected ASN.1 SEQUENCE (0x30), got 0x" + Integer .toHexString (tag ));
66+ }
67+ int seqLen = readLength (c );
68+ int seqEnd = c .pos + seqLen ;
69+ if (seqEnd > der .length ) {
70+ throw new SDKException ("hybrid wrapped key envelope length exceeds buffer" );
71+ }
72+ if (seqEnd != der .length ) {
73+ throw new SDKException ("hybrid wrapped key envelope has trailing bytes" );
74+ }
75+ byte [] hybridCt = readImplicitOctetString (c , 0 );
76+ byte [] encDek = readImplicitOctetString (c , 1 );
77+ if (c .pos != seqEnd ) {
78+ throw new SDKException ("hybrid wrapped key envelope SEQUENCE has trailing bytes" );
79+ }
80+ return new byte [][] { hybridCt , encDek };
81+ }
82+
83+ private static byte [] readImplicitOctetString (Cursor c , int expectedTagNo ) {
84+ int expectedTag = TAG_CONTEXT_PRIMITIVE_0 | expectedTagNo ;
85+ int tag = c .readByte ();
86+ if (tag != expectedTag ) {
87+ throw new SDKException ("expected context tag " + expectedTagNo
88+ + " (0x" + Integer .toHexString (expectedTag ) + ") but got 0x" + Integer .toHexString (tag ));
89+ }
90+ int len = readLength (c );
91+ if (c .pos + len > c .buf .length ) {
92+ throw new SDKException ("context-tagged element length exceeds buffer" );
93+ }
94+ byte [] out = new byte [len ];
95+ System .arraycopy (c .buf , c .pos , out , 0 , len );
96+ c .pos += len ;
97+ return out ;
98+ }
99+
100+ private static byte [] encodeTLV (int tag , byte [] content ) {
101+ byte [] lenBytes = encodeLength (content .length );
102+ byte [] out = new byte [1 + lenBytes .length + content .length ];
103+ out [0 ] = (byte ) tag ;
104+ System .arraycopy (lenBytes , 0 , out , 1 , lenBytes .length );
105+ System .arraycopy (content , 0 , out , 1 + lenBytes .length , content .length );
106+ return out ;
107+ }
108+
109+ private static byte [] encodeLength (int len ) {
110+ if (len < 0 ) {
111+ throw new SDKException ("negative ASN.1 length: " + len );
112+ }
113+ if (len < 0x80 ) {
114+ return new byte [] { (byte ) len };
115+ }
116+ // Long form: 0x80 | numBytes, then big-endian length bytes.
117+ int numBytes = 0 ;
118+ int tmp = len ;
119+ while (tmp > 0 ) { numBytes ++; tmp >>>= 8 ; }
120+ byte [] out = new byte [1 + numBytes ];
121+ out [0 ] = (byte ) (0x80 | numBytes );
122+ for (int i = numBytes ; i > 0 ; i --) {
123+ out [i ] = (byte ) (len & 0xFF );
124+ len >>>= 8 ;
92125 }
126+ return out ;
93127 }
94128
95- private static byte [] readImplicitOctetString (org .bouncycastle .asn1 .ASN1Encodable enc , int expectedTag ) {
96- ASN1TaggedObject tagged = ASN1TaggedObject .getInstance (enc );
97- if (tagged .getTagNo () != expectedTag ) {
98- throw new SDKException ("expected context tag " + expectedTag + " but got " + tagged .getTagNo ());
129+ private static int readLength (Cursor c ) {
130+ int first = c .readByte ();
131+ if ((first & 0x80 ) == 0 ) {
132+ return first ;
133+ }
134+ int numBytes = first & 0x7F ;
135+ if (numBytes == 0 || numBytes > 4 ) {
136+ // indefinite-length (numBytes == 0) is BER-only; DER rejects it.
137+ // > 4 would overflow a positive 32-bit int and is implausible for our envelope.
138+ throw new SDKException ("invalid ASN.1 length encoding: numBytes=" + numBytes );
139+ }
140+ int len = 0 ;
141+ for (int i = 0 ; i < numBytes ; i ++) {
142+ len = (len << 8 ) | c .readByte ();
143+ }
144+ if (len < 0 ) {
145+ throw new SDKException ("ASN.1 length overflowed signed int" );
99146 }
100- return org .bouncycastle .asn1 .ASN1OctetString .getInstance (tagged , false ).getOctets ();
147+ return len ;
148+ }
149+
150+ private static final class Cursor {
151+ final byte [] buf ;
152+ int pos ;
153+ Cursor (byte [] buf , int pos ) { this .buf = buf ; this .pos = pos ; }
154+ int readByte () {
155+ if (pos >= buf .length ) {
156+ throw new SDKException ("unexpected end of ASN.1 input at offset " + pos );
157+ }
158+ return buf [pos ++] & 0xFF ;
159+ }
160+ }
161+
162+ private static byte [] concat (byte [] a , byte [] b ) {
163+ byte [] out = new byte [a .length + b .length ];
164+ System .arraycopy (a , 0 , out , 0 , a .length );
165+ System .arraycopy (b , 0 , out , a .length , b .length );
166+ return out ;
101167 }
102168
103169 /**
104- * HKDF-SHA256 → 32-byte AES wrap key. {@code salt=null} substitutes the default TDF salt.
170+ * HKDF-SHA256 → 32-byte AES wrap key. Delegates to
171+ * {@link ECKeyPair#calculateHKDF(byte[], byte[])} (HKDF-Extract + Expand,
172+ * empty info, L = 32 — the parameters all three hybrid algorithms use).
105173 */
106- static byte [] deriveWrapKey (byte [] combinedSecret , byte [] salt , byte [] info ) {
107- byte [] effSalt = (salt == null || salt .length == 0 ) ? defaultTDFSalt () : salt ;
108- HKDFBytesGenerator hkdf = new HKDFBytesGenerator (new SHA256Digest ());
109- hkdf .init (new HKDFParameters (combinedSecret , effSalt , info ));
110- byte [] out = new byte [WRAP_KEY_SIZE ];
111- hkdf .generateBytes (out , 0 , out .length );
112- return out ;
174+ static byte [] deriveWrapKey (byte [] combinedSecret ) {
175+ return ECKeyPair .calculateHKDF (defaultTDFSalt (), combinedSecret );
113176 }
114177
115178 /**
0 commit comments