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.File;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.nio.ByteBuffer;
028import java.nio.channels.Channels;
029import java.nio.channels.WritableByteChannel;
030import java.nio.file.Files;
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.List;
034
035import org.apache.directory.server.i18n.I18n;
036
037
038/**
039 * Keytab file. The format is the following :
040 * <pre>
041 * { 
042 *   version : 2 bytes (0x05 0x02)
043 *   keytabEntry*
044 * }
045 *
046 * keytab_entry 
047 * {
048 *     size : int
049 *     numComponents :  short
050 *     realm : countedOctetString
051 *     components[numComponents] : countedOctetString
052 *     nameType : int
053 *     timestamp : int
054 *     vno8 : byte
055 *     key : keyBlock
056 *     vno : int // only present if &gt;= 4 bytes left in entry
057 * };
058 *
059 * keyblock 
060 * {
061 *     type : int
062 *     data : countedOctetString
063 * }
064 *
065 * countedOctetString 
066 * {
067 *     length : short
068 *     data[length] : bytes
069 * }
070 * </pre>
071 *
072 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
073 */
074public class Keytab
075{
076    /**
077     * Byte array constant for keytab file format 5.1.
078     */
079    public static final byte[] VERSION_0X501_BYTES = new byte[]
080        { ( byte ) 0x05, ( byte ) 0x01 };
081
082    // Format 0x0501
083    public static final short VERSION_0X501 = 0x0501;
084
085    /**
086     * Byte array constant for keytab file format 5.2.
087     */
088    public static final byte[] VERSION_0X502_BYTES = new byte[]
089        { ( byte ) 0x05, ( byte ) 0x02 };
090
091    // Format 0x0502
092    public static final short VERSION_0X502 = 0x0502;
093
094    private byte[] keytabVersion = VERSION_0X502_BYTES;
095    private List<KeytabEntry> entries = new ArrayList<>();
096
097
098    /**
099     * Read a keytab file.
100     *
101     * @param file
102     * @return The keytab.
103     * @throws IOException
104     */
105    public static Keytab read( File file ) throws IOException
106    {
107        ByteBuffer buffer = ByteBuffer.wrap( getBytesFromFile( file ) );
108        return readKeytab( buffer );
109    }
110
111
112    /**
113     * Returns a new instance of a keytab with the version
114     * defaulted to 5.2.
115     *
116     * @return The keytab.
117     */
118    public static Keytab getInstance()
119    {
120        return new Keytab();
121    }
122
123
124    /**
125     * Write the keytab to a {@link File}.
126     *
127     * @param file
128     * @throws IOException
129     */
130    public void write( File file ) throws IOException
131    {
132        KeytabEncoder writer = new KeytabEncoder();
133        ByteBuffer buffer = writer.write( keytabVersion, entries );
134        writeFile( buffer, file );
135    }
136
137
138    /**
139     * @param entries The entries to set.
140     */
141    public void setEntries( List<KeytabEntry> entries )
142    {
143        this.entries = entries;
144    }
145
146
147    /**
148     * @param keytabVersion The keytabVersion to set.
149     */
150    public void setKeytabVersion( byte[] keytabVersion )
151    {
152        this.keytabVersion = keytabVersion;
153    }
154
155
156    /**
157     * @return The entries.
158     */
159    public List<KeytabEntry> getEntries()
160    {
161        return Collections.unmodifiableList( entries );
162    }
163
164
165    /**
166     * @return The keytabVersion.
167     */
168    public byte[] getKeytabVersion()
169    {
170        return keytabVersion;
171    }
172
173
174    /**
175     * Read bytes into a keytab.
176     *
177     * @param bytes
178     * @return The keytab.
179     */
180    static Keytab read( byte[] bytes ) throws IOException
181    {
182        ByteBuffer buffer = ByteBuffer.wrap( bytes );
183        return readKeytab( buffer );
184    }
185
186
187    /**
188     * Write the keytab to a {@link ByteBuffer}.
189     * @return The buffer.
190     */
191    ByteBuffer write()
192    {
193        KeytabEncoder writer = new KeytabEncoder();
194        return writer.write( keytabVersion, entries );
195    }
196
197
198    /**
199     * Read the contents of the buffer into a keytab.
200     *
201     * @param buffer
202     * @return The keytab.
203     */
204    private static Keytab readKeytab( ByteBuffer buffer ) throws IOException
205    {
206        KeytabDecoder reader = new KeytabDecoder();
207        byte[] keytabVersion = reader.getKeytabVersion( buffer );
208        List<KeytabEntry> entries = reader.getKeytabEntries( buffer );
209
210        Keytab keytab = new Keytab();
211
212        keytab.setKeytabVersion( keytabVersion );
213        keytab.setEntries( entries );
214
215        return keytab;
216    }
217
218
219    /**
220     * Returns the contents of the {@link File} in a byte array.
221     *
222     * @param file
223     * @return The byte array of the file contents.
224     * @throws IOException
225     */
226    protected static byte[] getBytesFromFile( File file ) throws IOException
227    {
228        try ( InputStream is = Files.newInputStream( file.toPath() ) )
229        {
230
231            long length = file.length();
232
233            // Check to ensure that file is not larger than Integer.MAX_VALUE.
234            if ( length > Integer.MAX_VALUE )
235            {
236                throw new IOException( I18n.err( I18n.ERR_618, file.getName() ) );
237            }
238
239            // Create the byte array to hold the data.
240            byte[] bytes = new byte[( int ) length];
241
242            // Read in the bytes
243            int offset = 0;
244            int numRead = 0;
245            while ( offset < bytes.length && ( numRead = is.read( bytes, offset, bytes.length - offset ) ) >= 0 )
246            {
247                offset += numRead;
248            }
249
250            // Ensure all the bytes have been read in.
251            if ( offset < bytes.length )
252            {
253                throw new IOException( I18n.err( I18n.ERR_619, file.getName() ) );
254            }
255
256            return bytes;
257        }
258    }
259
260
261    /**
262     * Write the contents of the {@link ByteBuffer} to a {@link File}.
263     *
264     * @param buffer
265     * @param file
266     * @throws IOException
267     */
268    protected void writeFile( ByteBuffer buffer, File file ) throws IOException
269    {
270        // Set append false to replace existing.
271        OutputStream out = Files.newOutputStream( file.toPath() );
272
273        try ( WritableByteChannel channel = Channels.newChannel( out ) )
274        {
275            // Write the bytes between the position and limit.
276            channel.write( buffer );
277        }
278        finally
279        {
280            out.close();
281        }
282    }
283}