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