22 * The MIT License
33 *
44 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
5+ * Copyright (c) 2016, CloudBees Inc.
56 *
67 * Permission is hereby granted, free of charge, to any person obtaining a copy
78 * of this software and associated documentation files (the "Software"), to deal
2930import com .thoughtworks .xstream .io .HierarchicalStreamReader ;
3031import com .thoughtworks .xstream .io .HierarchicalStreamWriter ;
3132import com .trilead .ssh2 .crypto .Base64 ;
33+ import java .util .Arrays ;
3234import jenkins .model .Jenkins ;
3335import hudson .Util ;
3436import jenkins .security .CryptoConfidentialKey ;
3537import org .kohsuke .stapler .Stapler ;
3638
37- import javax .crypto .SecretKey ;
3839import javax .crypto .Cipher ;
3940import java .io .Serializable ;
4041import java .io .UnsupportedEncodingException ;
4445import org .kohsuke .accmod .Restricted ;
4546import org .kohsuke .accmod .restrictions .NoExternalUse ;
4647
48+ import static java .nio .charset .StandardCharsets .UTF_8 ;
49+
4750/**
4851 * Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret.
4952 *
5861 * @author Kohsuke Kawaguchi
5962 */
6063public final class Secret implements Serializable {
64+ private static final byte PAYLOAD_V1 = 1 ;
6165 /**
6266 * Unencrypted secret text.
6367 */
6468 private final String value ;
69+ private byte [] iv ;
70+
71+ /*package*/ Secret (String value ) {
72+ this .value = value ;
73+ }
6574
66- private Secret (String value ) {
75+ /*package*/ Secret (String value , byte [] iv ) {
6776 this .value = value ;
77+ this .iv = iv ;
6878 }
6979
7080 /**
@@ -100,77 +110,102 @@ public int hashCode() {
100110 return value .hashCode ();
101111 }
102112
103- /**
104- * Turns {@link Jenkins#getSecretKey()} into an AES key.
105- *
106- * @deprecated
107- * This is no longer the key we use to encrypt new information, but we still need this
108- * to be able to decrypt what's already persisted.
109- */
110- @ Deprecated
111- /*package*/ static SecretKey getLegacyKey () throws GeneralSecurityException {
112- String secret = SECRET ;
113- if (secret ==null ) return Jenkins .getInstance ().getSecretKeyAsAES128 ();
114- return Util .toAes128Key (secret );
115- }
116-
117113 /**
118114 * Encrypts {@link #value} and returns it in an encoded printable form.
119115 *
120116 * @see #toString()
121117 */
122118 public String getEncryptedValue () {
123119 try {
124- Cipher cipher = KEY .encrypt ();
125- // add the magic suffix which works like a check sum.
126- return new String (Base64 .encode (cipher .doFinal ((value +MAGIC ).getBytes ("UTF-8" ))));
120+ synchronized (this ) {
121+ if (iv == null ) { //if we were created from plain text or other reason without iv
122+ iv = KEY .newIv ();
123+ }
124+ }
125+ Cipher cipher = KEY .encrypt (iv );
126+ byte [] encrypted = cipher .doFinal (this .value .getBytes (UTF_8 ));
127+ byte [] payload = new byte [1 + 8 + iv .length + encrypted .length ];
128+ int pos = 0 ;
129+ // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
130+ payload [pos ++] = PAYLOAD_V1 ;
131+ payload [pos ++] = (byte )(iv .length >> 24 );
132+ payload [pos ++] = (byte )(iv .length >> 16 );
133+ payload [pos ++] = (byte )(iv .length >> 8 );
134+ payload [pos ++] = (byte )(iv .length );
135+ payload [pos ++] = (byte )(encrypted .length >> 24 );
136+ payload [pos ++] = (byte )(encrypted .length >> 16 );
137+ payload [pos ++] = (byte )(encrypted .length >> 8 );
138+ payload [pos ++] = (byte )(encrypted .length );
139+ System .arraycopy (iv , 0 , payload , pos , iv .length );
140+ pos +=iv .length ;
141+ System .arraycopy (encrypted , 0 , payload , pos , encrypted .length );
142+ return "{" +new String (Base64 .encode (payload ))+"}" ;
127143 } catch (GeneralSecurityException e ) {
128144 throw new Error (e ); // impossible
129- } catch (UnsupportedEncodingException e ) {
130- throw new Error (e ); // impossible
131145 }
132146 }
133147
134148 /**
135- * Pattern matching a possible output of {@link #getEncryptedValue}.
136- * Basically, any Base64-encoded value.
137- * You must then call {@link #decrypt} to eliminate false positives.
149+ * Pattern matching a possible output of {@link #getEncryptedValue}
150+ * Basically, any Base64-encoded value optionally wrapped by {@code {}}.
151+ * You must then call {@link #decrypt(String)} to eliminate false positives.
152+ * @see #ENCRYPTED_VALUE_PATTERN
138153 */
139154 @ Restricted (NoExternalUse .class )
140- public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern .compile ("[A-Za-z0-9+/]+={0,2}" );
155+ public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern .compile ("\\ {? [A-Za-z0-9+/]+={0,2}}? " );
141156
142157 /**
143158 * Reverse operation of {@link #getEncryptedValue()}. Returns null
144159 * if the given cipher text was invalid.
145160 */
146161 public static Secret decrypt (String data ) {
147- if (data ==null ) return null ;
148- try {
149- byte [] in = Base64 .decode (data .toCharArray ());
150- Secret s = tryDecrypt (KEY .decrypt (), in );
151- if (s !=null ) return s ;
162+ if (data == null ) return null ;
152163
153- // try our historical key for backward compatibility
154- Cipher cipher = getCipher ("AES" );
155- cipher .init (Cipher .DECRYPT_MODE , getLegacyKey ());
156- return tryDecrypt (cipher , in );
157- } catch (GeneralSecurityException e ) {
158- return null ;
159- } catch (UnsupportedEncodingException e ) {
160- throw new Error (e ); // impossible
161- } catch (IOException e ) {
162- return null ;
163- }
164- }
165-
166- /*package*/ static Secret tryDecrypt (Cipher cipher , byte [] in ) throws UnsupportedEncodingException {
167- try {
168- String plainText = new String (cipher .doFinal (in ), "UTF-8" );
169- if (plainText .endsWith (MAGIC ))
170- return new Secret (plainText .substring (0 ,plainText .length ()-MAGIC .length ()));
171- return null ;
172- } catch (GeneralSecurityException e ) {
173- return null ; // if the key doesn't match with the bytes, it can result in BadPaddingException
164+ if (data .startsWith ("{" ) && data .endsWith ("}" )) { //likely CBC encrypted/containing metadata but could be plain text
165+ byte [] payload ;
166+ try {
167+ payload = Base64 .decode (data .substring (1 , data .length ()-1 ).toCharArray ());
168+ } catch (IOException e ) {
169+ return null ;
170+ }
171+ switch (payload [0 ]) {
172+ case PAYLOAD_V1 :
173+ // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
174+ int ivLength = ((payload [1 ] & 0xff ) << 24 )
175+ | ((payload [2 ] & 0xff ) << 16 )
176+ | ((payload [3 ] & 0xff ) << 8 )
177+ | (payload [4 ] & 0xff );
178+ int dataLength = ((payload [5 ] & 0xff ) << 24 )
179+ | ((payload [6 ] & 0xff ) << 16 )
180+ | ((payload [7 ] & 0xff ) << 8 )
181+ | (payload [8 ] & 0xff );
182+ if (payload .length != 1 + 8 + ivLength + dataLength ) {
183+ // not valid v1
184+ return null ;
185+ }
186+ byte [] iv = Arrays .copyOfRange (payload , 9 , 9 + ivLength );
187+ byte [] code = Arrays .copyOfRange (payload , 9 +ivLength , payload .length );
188+ String text ;
189+ try {
190+ text = new String (KEY .decrypt (iv ).doFinal (code ), UTF_8 );
191+ } catch (GeneralSecurityException e ) {
192+ // it's v1 which cannot be historical, but not decrypting
193+ return null ;
194+ }
195+ return new Secret (text , iv );
196+ default :
197+ return null ;
198+ }
199+ } else {
200+ try {
201+ return HistoricalSecrets .decrypt (data , KEY );
202+ } catch (GeneralSecurityException e ) {
203+ return null ;
204+ } catch (UnsupportedEncodingException e ) {
205+ throw new Error (e ); // impossible
206+ } catch (IOException e ) {
207+ return null ;
208+ }
174209 }
175210 }
176211
@@ -228,8 +263,6 @@ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingCont
228263 }
229264 }
230265
231- private static final String MAGIC = "::::MAGIC::::" ;
232-
233266 /**
234267 * Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862
235268 * @see #getCipher(String)
@@ -246,6 +279,14 @@ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingCont
246279 */
247280 private static final CryptoConfidentialKey KEY = new CryptoConfidentialKey (Secret .class .getName ());
248281
282+ /**
283+ * Reset the internal secret key for testing.
284+ */
285+ @ Restricted (NoExternalUse .class )
286+ /*package*/ static void resetKeyForTest () {
287+ KEY .resetForTest ();
288+ }
289+
249290 private static final long serialVersionUID = 1L ;
250291
251292 static {
0 commit comments