1package org.bouncycastle.asn1;
2
3import java.io.IOException;
4import java.text.ParseException;
5import java.text.SimpleDateFormat;
6// Android-added: Localization support
7import java.util.Calendar;
8import java.util.Date;
9import java.util.Locale;
10import java.util.SimpleTimeZone;
11import java.util.TimeZone;
12
13import org.bouncycastle.util.Arrays;
14import org.bouncycastle.util.Strings;
15
16/**
17 * Base class representing the ASN.1 GeneralizedTime type.
18 * <p>
19 * The main difference between these and UTC time is a 4 digit year.
20 * </p>
21 */
22public class ASN1GeneralizedTime
23    extends ASN1Primitive
24{
25    private byte[] time;
26
27    /**
28     * return a generalized time from the passed in object
29     *
30     * @param obj an ASN1GeneralizedTime or an object that can be converted into one.
31     * @return an ASN1GeneralizedTime instance, or null.
32     * @throws IllegalArgumentException if the object cannot be converted.
33     */
34    public static ASN1GeneralizedTime getInstance(
35        Object obj)
36    {
37        if (obj == null || obj instanceof ASN1GeneralizedTime)
38        {
39            return (ASN1GeneralizedTime)obj;
40        }
41
42        if (obj instanceof byte[])
43        {
44            try
45            {
46                return (ASN1GeneralizedTime)fromByteArray((byte[])obj);
47            }
48            catch (Exception e)
49            {
50                throw new IllegalArgumentException("encoding error in getInstance: " + e.toString());
51            }
52        }
53
54        throw new IllegalArgumentException("illegal object in getInstance: " + obj.getClass().getName());
55    }
56
57    /**
58     * return a Generalized Time object from a tagged object.
59     *
60     * @param obj      the tagged object holding the object we want
61     * @param explicit true if the object is meant to be explicitly
62     *                 tagged false otherwise.
63     * @return an ASN1GeneralizedTime instance.
64     * @throws IllegalArgumentException if the tagged object cannot
65     * be converted.
66     */
67    public static ASN1GeneralizedTime getInstance(
68        ASN1TaggedObject obj,
69        boolean explicit)
70    {
71        ASN1Primitive o = obj.getObject();
72
73        if (explicit || o instanceof ASN1GeneralizedTime)
74        {
75            return getInstance(o);
76        }
77        else
78        {
79            return new ASN1GeneralizedTime(((ASN1OctetString)o).getOctets());
80        }
81    }
82
83    /**
84     * The correct format for this is YYYYMMDDHHMMSS[.f]Z, or without the Z
85     * for local time, or Z+-HHMM on the end, for difference between local
86     * time and UTC time. The fractional second amount f must consist of at
87     * least one number with trailing zeroes removed.
88     *
89     * @param time the time string.
90     * @throws IllegalArgumentException if String is an illegal format.
91     */
92    public ASN1GeneralizedTime(
93        String time)
94    {
95        this.time = Strings.toByteArray(time);
96        try
97        {
98            this.getDate();
99        }
100        catch (ParseException e)
101        {
102            throw new IllegalArgumentException("invalid date string: " + e.getMessage());
103        }
104    }
105
106    /**
107     * Base constructor from a java.util.date object
108     *
109     * @param time a date object representing the time of interest.
110     */
111    public ASN1GeneralizedTime(
112        Date time)
113    {
114        // Android-changed: Use localized version
115        // SimpleDateFormat dateF = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
116        SimpleDateFormat dateF = new SimpleDateFormat("yyyyMMddHHmmss'Z'", Locale.US);
117
118        dateF.setTimeZone(new SimpleTimeZone(0, "Z"));
119
120        this.time = Strings.toByteArray(dateF.format(time));
121    }
122
123    /**
124     * Base constructor from a java.util.date and Locale - you may need to use this if the default locale
125     * doesn't use a Gregorian calender so that the GeneralizedTime produced is compatible with other ASN.1 implementations.
126     *
127     * @param time a date object representing the time of interest.
128     * @param locale an appropriate Locale for producing an ASN.1 GeneralizedTime value.
129     */
130    public ASN1GeneralizedTime(
131        Date time,
132        Locale locale)
133    {
134        // BEGIN Android-changed: Use localized version
135        // SimpleDateFormat dateF = new SimpleDateFormat("yyyyMMddHHmmss'Z'", locale);
136        SimpleDateFormat dateF = new SimpleDateFormat("yyyyMMddHHmmss'Z'", Locale.US);
137        dateF.setCalendar(Calendar.getInstance(Locale.US));
138        // END Android-changed: Use localized version
139
140        dateF.setTimeZone(new SimpleTimeZone(0, "Z"));
141
142        this.time = Strings.toByteArray(dateF.format(time));
143    }
144
145    ASN1GeneralizedTime(
146        byte[] bytes)
147    {
148        this.time = bytes;
149    }
150
151    /**
152     * Return the time.
153     *
154     * @return The time string as it appeared in the encoded object.
155     */
156    public String getTimeString()
157    {
158        return Strings.fromByteArray(time);
159    }
160
161    /**
162     * return the time - always in the form of
163     * YYYYMMDDhhmmssGMT(+hh:mm|-hh:mm).
164     * <p>
165     * Normally in a certificate we would expect "Z" rather than "GMT",
166     * however adding the "GMT" means we can just use:
167     * <pre>
168     *     dateF = new SimpleDateFormat("yyyyMMddHHmmssz");
169     * </pre>
170     * To read in the time and get a date which is compatible with our local
171     * time zone.
172     * </p>
173     * @return a String representation of the time.
174     */
175    public String getTime()
176    {
177        String stime = Strings.fromByteArray(time);
178
179        //
180        // standardise the format.
181        //
182        if (stime.charAt(stime.length() - 1) == 'Z')
183        {
184            return stime.substring(0, stime.length() - 1) + "GMT+00:00";
185        }
186        else
187        {
188            int signPos = stime.length() - 5;
189            char sign = stime.charAt(signPos);
190            if (sign == '-' || sign == '+')
191            {
192                return stime.substring(0, signPos)
193                    + "GMT"
194                    + stime.substring(signPos, signPos + 3)
195                    + ":"
196                    + stime.substring(signPos + 3);
197            }
198            else
199            {
200                signPos = stime.length() - 3;
201                sign = stime.charAt(signPos);
202                if (sign == '-' || sign == '+')
203                {
204                    return stime.substring(0, signPos)
205                        + "GMT"
206                        + stime.substring(signPos)
207                        + ":00";
208                }
209            }
210        }
211        return stime + calculateGMTOffset();
212    }
213
214    private String calculateGMTOffset()
215    {
216        String sign = "+";
217        TimeZone timeZone = TimeZone.getDefault();
218        int offset = timeZone.getRawOffset();
219        if (offset < 0)
220        {
221            sign = "-";
222            offset = -offset;
223        }
224        int hours = offset / (60 * 60 * 1000);
225        int minutes = (offset - (hours * 60 * 60 * 1000)) / (60 * 1000);
226
227        try
228        {
229            if (timeZone.useDaylightTime() && timeZone.inDaylightTime(this.getDate()))
230            {
231                hours += sign.equals("+") ? 1 : -1;
232            }
233        }
234        catch (ParseException e)
235        {
236            // we'll do our best and ignore daylight savings
237        }
238
239        return "GMT" + sign + convert(hours) + ":" + convert(minutes);
240    }
241
242    private String convert(int time)
243    {
244        if (time < 10)
245        {
246            return "0" + time;
247        }
248
249        return Integer.toString(time);
250    }
251
252    public Date getDate()
253        throws ParseException
254    {
255        SimpleDateFormat dateF;
256        String stime = Strings.fromByteArray(time);
257        String d = stime;
258
259        if (stime.endsWith("Z"))
260        {
261            if (hasFractionalSeconds())
262            {
263                // Android-changed: Use localized version
264                // dateF = new SimpleDateFormat("yyyyMMddHHmmss.SSS'Z'");
265                dateF = new SimpleDateFormat("yyyyMMddHHmmss.SSS'Z'", Locale.US);
266            }
267            else
268            {
269                // Android-changed: Use localized version
270                // dateF = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
271                dateF = new SimpleDateFormat("yyyyMMddHHmmss'Z'", Locale.US);
272            }
273
274            dateF.setTimeZone(new SimpleTimeZone(0, "Z"));
275        }
276        else if (stime.indexOf('-') > 0 || stime.indexOf('+') > 0)
277        {
278            d = this.getTime();
279            if (hasFractionalSeconds())
280            {
281                // Android-changed: Use localized version
282                // dateF = new SimpleDateFormat("yyyyMMddHHmmss.SSSz");
283                dateF = new SimpleDateFormat("yyyyMMddHHmmss.SSSz", Locale.US);
284            }
285            else
286            {
287                // Android-changed: Use localized version
288                // dateF = new SimpleDateFormat("yyyyMMddHHmmssz");
289                dateF = new SimpleDateFormat("yyyyMMddHHmmssz", Locale.US);
290            }
291
292            dateF.setTimeZone(new SimpleTimeZone(0, "Z"));
293        }
294        else
295        {
296            if (hasFractionalSeconds())
297            {
298                // Android-changed: Use localized version
299                // dateF = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
300                dateF = new SimpleDateFormat("yyyyMMddHHmmss.SSS", Locale.US);
301            }
302            else
303            {
304                // Android-changed: Use localized version
305                // dateF = new SimpleDateFormat("yyyyMMddHHmmss");
306                dateF = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
307            }
308
309            dateF.setTimeZone(new SimpleTimeZone(0, TimeZone.getDefault().getID()));
310        }
311
312        if (hasFractionalSeconds())
313        {
314            // java misinterprets extra digits as being milliseconds...
315            String frac = d.substring(14);
316            int index;
317            for (index = 1; index < frac.length(); index++)
318            {
319                char ch = frac.charAt(index);
320                if (!('0' <= ch && ch <= '9'))
321                {
322                    break;
323                }
324            }
325
326            if (index - 1 > 3)
327            {
328                frac = frac.substring(0, 4) + frac.substring(index);
329                d = d.substring(0, 14) + frac;
330            }
331            else if (index - 1 == 1)
332            {
333                frac = frac.substring(0, index) + "00" + frac.substring(index);
334                d = d.substring(0, 14) + frac;
335            }
336            else if (index - 1 == 2)
337            {
338                frac = frac.substring(0, index) + "0" + frac.substring(index);
339                d = d.substring(0, 14) + frac;
340            }
341        }
342
343        return dateF.parse(d);
344    }
345
346    private boolean hasFractionalSeconds()
347    {
348        for (int i = 0; i != time.length; i++)
349        {
350            if (time[i] == '.')
351            {
352                if (i == 14)
353                {
354                    return true;
355                }
356            }
357        }
358        return false;
359    }
360
361    boolean isConstructed()
362    {
363        return false;
364    }
365
366    int encodedLength()
367    {
368        int length = time.length;
369
370        return 1 + StreamUtil.calculateBodyLength(length) + length;
371    }
372
373    void encode(
374        ASN1OutputStream out)
375        throws IOException
376    {
377        out.writeEncoded(BERTags.GENERALIZED_TIME, time);
378    }
379
380    boolean asn1Equals(
381        ASN1Primitive o)
382    {
383        if (!(o instanceof ASN1GeneralizedTime))
384        {
385            return false;
386        }
387
388        return Arrays.areEqual(time, ((ASN1GeneralizedTime)o).time);
389    }
390
391    public int hashCode()
392    {
393        return Arrays.hashCode(time);
394    }
395}
396