001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *  
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *  
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License. 
018 *  
019 */
020package org.apache.directory.server.kerberos.shared.keytab;
021
022
023import java.io.UnsupportedEncodingException;
024import java.nio.ByteBuffer;
025import java.util.ArrayList;
026import java.util.List;
027
028import org.apache.directory.shared.kerberos.components.EncryptionKey;
029
030
031/**
032 * Encode keytab fields into a {@link ByteBuffer}.
033 *
034 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
035 */
036public class KeytabEncoder
037{
038    /**
039     * Tells if the keytabCersion is 0x0501 or 0x0502
040     */
041    private short getKeytabVersion( byte[] version )
042    {
043        if ( ( version != null ) && ( version.length == 2 ) && ( version[0] == 0x05 ) )
044        {
045            switch ( version[1] )
046            {
047                case 0x01:
048                    return Keytab.VERSION_0X501;
049
050                case 0x02:
051                    return Keytab.VERSION_0X502;
052
053                default:
054                    return -1;
055            }
056        }
057
058        return -1;
059    }
060
061
062    /**
063     * Write the keytab version and entries into a {@link ByteBuffer}.
064     *
065     * @param keytabVersion
066     * @param entries
067     * @return The ByteBuffer.
068     */
069    public ByteBuffer write( byte[] keytabVersion, List<KeytabEntry> entries )
070    {
071        List<ByteBuffer> keytabEntryBuffers = new ArrayList<>();
072        short version = getKeytabVersion( keytabVersion );
073
074        int buffersSize = encodeKeytabEntries( keytabEntryBuffers, version, entries );
075
076        ByteBuffer buffer = ByteBuffer.allocate(
077            keytabVersion.length + buffersSize );
078
079        // The keytab version (0x0502 or 0x5001)
080        buffer.put( keytabVersion );
081
082        for ( ByteBuffer keytabEntryBuffer : keytabEntryBuffers )
083        {
084            // The buffer
085            buffer.put( keytabEntryBuffer );
086        }
087
088        buffer.flip();
089
090        return buffer;
091    }
092
093
094    /**
095     * Encode the keytab entries. Each entry stores :
096     * - the size
097     * - the principal name
098     * - the type (int, 4 bytes)
099     * - the timestamp (int, 4 bytes)
100     * - the key version (1 byte)
101     * - the key 
102     *
103     * @param buffer
104     * @param entries
105     */
106    private int encodeKeytabEntries( List<ByteBuffer> buffers, short version, List<KeytabEntry> entries )
107    {
108        int size = 0;
109
110        for ( KeytabEntry keytabEntry : entries )
111        {
112            ByteBuffer entryBuffer = encodeKeytabEntry( version, keytabEntry );
113
114            buffers.add( entryBuffer );
115
116            // The buffer size
117            size += entryBuffer.limit();
118        }
119
120        return size;
121    }
122
123
124    /**
125     * Encode a "keytab entry," which consists of a principal name,
126     * principal type, key version number, and key material.
127     */
128    private ByteBuffer encodeKeytabEntry( short version, KeytabEntry entry )
129    {
130        // Compute the principalName encoding
131        ByteBuffer principalNameBuffer = encodePrincipalName( version, entry.getPrincipalName() );
132
133        // Compute the keyblock encoding
134        ByteBuffer keyBlockBuffer = encodeKeyBlock( entry.getKey() );
135
136        int bufferSize =
137            4 + // size
138                principalNameBuffer.limit() + // principalName size
139                4 + // timeStamp
140                1 + // keyVersion
141                keyBlockBuffer.limit(); // keyBlock size
142
143        if ( version == Keytab.VERSION_0X502 )
144        {
145            bufferSize += 4; // Add the principal NameType only for version 0x502
146        }
147
148        ByteBuffer buffer = ByteBuffer.allocate( bufferSize );
149
150        // Store the size
151        buffer.putInt( bufferSize - 4 );
152
153        // Store the principalNames
154        buffer.put( principalNameBuffer );
155
156        // Store the principal type if version == 0x0502
157        if ( version == Keytab.VERSION_0X502 )
158        {
159            buffer.putInt( ( int ) entry.getPrincipalType() );
160        }
161
162        // Store the timeStamp 
163        buffer.putInt( ( int ) ( entry.getTimeStamp().getTime() / 1000 ) );
164
165        // Store the key version
166        buffer.put( entry.getKeyVersion() );
167
168        // Store the KeyBlock
169        buffer.put( keyBlockBuffer );
170        buffer.flip();
171
172        return buffer;
173    }
174
175
176    /**
177     * Encode a principal name.
178     *
179     * @param buffer
180     * @param principalName
181     */
182    private ByteBuffer encodePrincipalName( short version, String principalName )
183    {
184        String[] split = principalName.split( "@" );
185        String nameComponentPart = split[0];
186        String realm = split[1];
187
188        String[] nameComponents = nameComponentPart.split( "/" );
189
190        // Compute the size of the buffer
191        List<byte[]> strings = new ArrayList<>();
192
193        // Initialize the size with the number of components' size
194        int size = 2;
195
196        size += encodeCountedString( strings, realm );
197
198        // compute NameComponents
199        for ( String nameComponent : nameComponents )
200        {
201            size += encodeCountedString( strings, nameComponent );
202        }
203
204        ByteBuffer buffer = ByteBuffer.allocate( size );
205
206        // Now, write the data into the buffer
207        // store the numComponents
208        if ( version == Keytab.VERSION_0X501 )
209        {
210            // increment for version 0x0501
211            buffer.putShort( ( short ) ( nameComponents.length + 1 ) );
212        }
213        else
214        {
215            // Version = OxO502
216            buffer.putShort( ( short ) ( nameComponents.length ) );
217        }
218
219        // Store the realm and the nameComponents
220        for ( byte[] string : strings )
221        {
222            buffer.putShort( ( short ) ( string.length ) );
223            buffer.put( string );
224        }
225
226        buffer.flip();
227
228        return buffer;
229    }
230
231
232    /**
233     * Encode a 16-bit encryption type and symmetric key material.
234     * 
235     * We store the KeyType value ( a short ) and the KeyValue ( a length
236     * on a short and the bytes )
237     */
238    private ByteBuffer encodeKeyBlock( EncryptionKey key )
239    {
240        byte[] keyBytes = key.getKeyValue();
241        int size = 2 + 2 + keyBytes.length; // type, length, data
242        ByteBuffer buffer = ByteBuffer.allocate( size );
243
244        // The type
245        buffer.putShort( ( short ) key.getKeyType().getValue() );
246
247        // Use a prefixed 16-bit length to encode raw bytes.
248        buffer.putShort( ( short ) keyBytes.length );
249        buffer.put( keyBytes );
250
251        buffer.flip();
252
253        return buffer;
254    }
255
256
257    /**
258     * Use a prefixed 16-bit length to encode a String.  Realm and name
259     * components are ASCII encoded text with no zero terminator.
260     */
261    private short encodeCountedString( List<byte[]> nameComponentBytes, String string )
262    {
263        try
264        {
265            byte[] data = string.getBytes( "US-ASCII" );
266            nameComponentBytes.add( data );
267
268            return ( short ) ( data.length + 2 );
269        }
270        catch ( UnsupportedEncodingException uee )
271        {
272            throw new RuntimeException( uee.getMessage(), uee );
273        }
274    }
275}