21 | * Examples:
22 | *
23 | *
24 | * Default unsecured configuration:
25 | *
26 | *
27 | * {@code 28 | * SimpleStorageConfiguration configuration = new SimpleStorageConfiguration.Builder() 29 | * .build() 30 | * } 31 | *32 | *
33 | * Secured configuration: 34 | *
35 | *
36 | * { 37 | * @code 38 | * final int CHUNK_SIZE = 16 * 1024; 39 | * final String IVX = "1234567890123456"; 40 | * final String SECRET_KEY = "secret1234567890"; 41 | * 42 | * SimpleStorageConfiguration configuration = new SimpleStorageConfiguration.Builder().setChuckSize(CHUNK_SIZE) 43 | * .setEncryptContent(IVX, SECRET_KEY).build(); 44 | * } 45 | *46 | * 47 | * @author Roman Kushnarenko - sromku (sromku@gmail.com) 48 | */ 49 | public class EncryptConfiguration { 50 | 51 | private final static String TAG = "EncryptConfiguration"; 52 | 53 | /** 54 | * The best chunk size: http://stackoverflow.com/a/237495/334522 55 | */ 56 | private int mChunkSize; 57 | private boolean mIsEncrypted; 58 | private byte[] mIvParameter; 59 | private byte[] mSecretKey; 60 | 61 | private EncryptConfiguration(Builder builder) { 62 | mChunkSize = builder._chunkSize; 63 | mIsEncrypted = builder._isEncrypted; 64 | mIvParameter = builder._ivParameter; 65 | mSecretKey = builder._secretKey; 66 | } 67 | 68 | /** 69 | * Get chunk size. The chuck size is used while reading the file by chunks 70 | * {@link FileInputStream#read(byte[], int, int)}. 71 | * 72 | * @return The chunk size 73 | */ 74 | public int getChuckSize() { 75 | return mChunkSize; 76 | } 77 | 78 | /** 79 | * Encrypt the file content.
139 | * The default: 8 * 1024 = 8192 bits
140 | *
141 | * @param chunkSize The chunk size in bits
142 | * @return The {@link Builder}
143 | */
144 | public Builder setChuckSize(int chunkSize) {
145 | _chunkSize = chunkSize;
146 | return this;
147 | }
148 |
149 | /**
150 | * Encrypt and descrypt the file content while writing and reading
151 | * to/from disc.
152 | *
153 | * @param ivx This is not have to be secret. It used just for better
154 | * randomizing the cipher. You have to use the same IV
155 | * parameter within the same encrypted and written files.
156 | * Means, if you want to have the same content after
157 | * descryption then the same IV must be used.
158 | *
159 | *
160 | * Important: The length must be 16 long
161 | *
162 | * About this parameter from wiki:
163 | * https://en.wikipedia.org
164 | * /wiki/Block_cipher_modes_of_operation
165 | * #Initialization_vector_.28IV.29
166 | *
167 | * @param secretKey Set the secret key for encryption of file content.
168 | *
169 | *
170 | * Important: The length must be 16 long
171 | *
172 | * Uses SHA-256 to generate a hash from your key and trim
173 | * the result to 128 bit (16 bytes)
11 | * http://developer.android.com/reference/javax/crypto/Cipher.html
12 | * http://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html
13 | * http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
14 | *
15 | * @author Roman Kushnarenko - sromku (sromku@gmail.com)
16 | */
17 | public enum CipherAlgorithmType {
18 | /**
19 | * Advanced Encryption Standard as specified by NIST in FIPS 197. Also known
20 | * as the Rijndael algorithm by Joan Daemen and Vincent Rijmen, AES is a
21 | * 128-bit block cipher supporting keys of 128, 192, and 256 bits.
22 | */
23 | AES("AES"),
24 |
25 | /**
26 | * The Digital Encryption Standard.
27 | */
28 | DES("DES"),
29 |
30 | /**
31 | * Triple DES Encryption (also known as DES-EDE, 3DES, or Triple-DES). Data
32 | * is encrypted using the DES algorithm three separate times. It is first
33 | * encrypted using the first subkey, then decrypted with the second subkey,
34 | * and encrypted with the third subkey.
35 | */
36 | DESede("DESede"),
37 |
38 | /**
39 | * The RSA encryption algorithm
40 | */
41 | RSA("RSA");
42 |
43 | private String mName;
44 |
45 | private CipherAlgorithmType(String name) {
46 | mName = name;
47 | }
48 |
49 | /**
50 | * Get the algorithm name of the enum value.
51 | *
52 | * @return The algorithm name
53 | */
54 | public String getAlgorithmName() {
55 | return mName;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/storage/src/main/java/com/snatik/storage/security/CipherModeType.java:
--------------------------------------------------------------------------------
1 | package com.snatik.storage.security;
2 |
3 | /**
4 | * https://en.wikipedia.org/wiki/Block_cipher_modes_of_operation
5 | *
6 | * @author Roman Kushnarenko - sromku (sromku@gmail.com)
7 | */
8 | public enum CipherModeType {
9 | /**
10 | * Cipher Block Chaining Mode
11 | */
12 | CBC("CBC"),
13 |
14 | /**
15 | * Electronic Codebook Mode
16 | */
17 | ECB("ECB");
18 |
19 | private String mName;
20 |
21 | private CipherModeType(String name) {
22 | mName = name;
23 | }
24 |
25 | /**
26 | * Get the algorithm name of the enum value.
27 | *
28 | * @return The algorithm name
29 | */
30 | public String getAlgorithmName() {
31 | return mName;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/storage/src/main/java/com/snatik/storage/security/CipherPaddingType.java:
--------------------------------------------------------------------------------
1 | package com.snatik.storage.security;
2 |
3 | /**
4 | * @author Roman Kushnarenko - sromku (sromku@gmail.com)
5 | */
6 | public enum CipherPaddingType {
7 | NoPadding("NoPadding"),
8 | PKCS5Padding("PKCS5Padding"),
9 | PKCS1Padding("PKCS1Padding"),
10 | OAEPWithSHA_1AndMGF1Padding("OAEPWithSHA-1AndMGF1Padding"),
11 | OAEPWithSHA_256AndMGF1Padding("OAEPWithSHA-256AndMGF1Padding");
12 |
13 | private String mName;
14 |
15 | private CipherPaddingType(String name) {
16 | mName = name;
17 | }
18 |
19 | /**
20 | * Get the algorithm name of the enum value.
21 | *
22 | * @return The algorithm name
23 | */
24 | public String getAlgorithmName() {
25 | return mName;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/storage/src/main/java/com/snatik/storage/security/CipherTransformationType.java:
--------------------------------------------------------------------------------
1 | package com.snatik.storage.security;
2 |
3 | /**
4 | * Supported types:
5 | * http://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html
174 | *
175 | * @see Block
177 | * cipher mode of operation
178 | */
179 | public Builder setEncryptContent(String ivx, String secretKey, byte[] salt) {
180 | _isEncrypted = true;
181 |
182 | // Set IV parameter
183 | try {
184 | _ivParameter = ivx.getBytes(UTF_8);
185 | } catch (UnsupportedEncodingException e) {
186 | Log.e(TAG, "UnsupportedEncodingException", e);
187 | }
188 |
189 | // Set secret key
190 | try {
191 | /*
192 | * We generate random salt and then use 1000 iterations to
193 | * initialize secret key factory which in-turn generates key.
194 | */
195 | int iterationCount = 1000; // recommended by PKCS#5
196 | int keyLength = 128;
197 |
198 | KeySpec keySpec = new PBEKeySpec(secretKey.toCharArray(), salt, iterationCount, keyLength);
199 | SecretKeyFactory keyFactory = null;
200 | if (Build.VERSION.SDK_INT >= 19) {
201 | // see:
202 | // http://android-developers.blogspot.co.il/2013/12/changes-to-secretkeyfactory-api-in.html
203 | // Use compatibility key factory -- only uses lower 8-bits
204 | // of passphrase chars
205 | keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1And8bit");
206 | } else {
207 | // Traditional key factory. Will use lower 8-bits of
208 | // passphrase chars on
209 | // older Android versions (API level 18 and lower) and all
210 | // available bits
211 | // on KitKat and newer (API level 19 and higher).
212 | keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
213 | }
214 | byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
215 |
216 | _secretKey = keyBytes;
217 |
218 | } catch (InvalidKeySpecException e) {
219 | Log.e(TAG, "InvalidKeySpecException", e);
220 | } catch (NoSuchAlgorithmException e) {
221 | Log.e(TAG, "NoSuchAlgorithmException", e);
222 | }
223 |
224 | return this;
225 | }
226 |
227 | }
228 |
229 | }
230 |
--------------------------------------------------------------------------------
/storage/src/main/java/com/snatik/storage/Storable.java:
--------------------------------------------------------------------------------
1 | package com.snatik.storage;
2 |
3 | /**
4 | * As it sounds, anything that can stored and represented as byte array.
5 | */
6 | public interface Storable {
7 | byte[] getBytes();
8 | }
9 |
--------------------------------------------------------------------------------
/storage/src/main/java/com/snatik/storage/Storage.java:
--------------------------------------------------------------------------------
1 | package com.snatik.storage;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.os.Build;
6 | import android.os.Environment;
7 | import android.os.StatFs;
8 | import android.util.Log;
9 |
10 | import com.snatik.storage.helpers.ImmutablePair;
11 | import com.snatik.storage.helpers.SizeUnit;
12 | import com.snatik.storage.security.SecurityUtil;
13 |
14 | import java.io.ByteArrayOutputStream;
15 | import java.io.Closeable;
16 | import java.io.File;
17 | import java.io.FileInputStream;
18 | import java.io.FileNotFoundException;
19 | import java.io.FileOutputStream;
20 | import java.io.FilenameFilter;
21 | import java.io.IOException;
22 | import java.io.OutputStream;
23 | import java.nio.channels.FileChannel;
24 | import java.util.ArrayList;
25 | import java.util.Arrays;
26 | import java.util.LinkedList;
27 | import java.util.List;
28 |
29 | import javax.crypto.Cipher;
30 |
31 | /**
32 | * Common class for internal and external storage implementations
33 | *
34 | * @author Roman Kushnarenko - sromku (sromku@gmail.com)
35 | */
36 | public class Storage {
37 |
38 | private static final String TAG = "Storage";
39 |
40 | private final Context mContext;
41 | private EncryptConfiguration mConfiguration;
42 |
43 | public Storage(Context context) {
44 | mContext = context;
45 | }
46 |
47 | public void setEncryptConfiguration(EncryptConfiguration configuration) {
48 | mConfiguration = configuration;
49 | }
50 |
51 | public String getExternalStorageDirectory() {
52 | return Environment.getExternalStorageDirectory().getAbsolutePath();
53 | }
54 |
55 | public String getExternalStorageDirectory(String publicDirectory) {
56 | return Environment.getExternalStoragePublicDirectory(publicDirectory).getAbsolutePath();
57 | }
58 |
59 | public String getInternalRootDirectory() {
60 | return Environment.getRootDirectory().getAbsolutePath();
61 | }
62 |
63 | public String getInternalFilesDirectory() {
64 | return mContext.getFilesDir().getAbsolutePath();
65 | }
66 |
67 | public String getInternalCacheDirectory() {
68 | return mContext.getCacheDir().getAbsolutePath();
69 | }
70 |
71 | public static boolean isExternalWritable() {
72 | String state = Environment.getExternalStorageState();
73 | if (Environment.MEDIA_MOUNTED.equals(state)) {
74 | return true;
75 | }
76 | return false;
77 | }
78 |
79 | public boolean createDirectory(String path) {
80 | File directory = new File(path);
81 | if (directory.exists()) {
82 | Log.w(TAG, "Directory '" + path + "' already exists");
83 | return false;
84 | }
85 | return directory.mkdirs();
86 | }
87 |
88 | public boolean createDirectory(String path, boolean override) {
89 |
90 | // Check if directory exists. If yes, then delete all directory
91 | if (override && isDirectoryExists(path)) {
92 | deleteDirectory(path);
93 | }
94 |
95 | // Create new directory
96 | return createDirectory(path);
97 | }
98 |
99 | public boolean deleteDirectory(String path) {
100 | return deleteDirectoryImpl(path);
101 | }
102 |
103 | public boolean isDirectoryExists(String path) {
104 | return new File(path).exists();
105 | }
106 |
107 | public boolean createFile(String path, String content) {
108 | return createFile(path, content.getBytes());
109 | }
110 |
111 | public boolean createFile(String path, Storable storable) {
112 | return createFile(path, storable.getBytes());
113 | }
114 |
115 | public boolean createFile(String path, byte[] content) {
116 | try {
117 | OutputStream stream = new FileOutputStream(new File(path));
118 |
119 | // encrypt if needed
120 | if (mConfiguration != null && mConfiguration.isEncrypted()) {
121 | content = encrypt(content, Cipher.ENCRYPT_MODE);
122 | }
123 |
124 | stream.write(content);
125 | stream.flush();
126 | stream.close();
127 | } catch (IOException e) {
128 | Log.e(TAG, "Failed create file", e);
129 | return false;
130 | }
131 | return true;
132 | }
133 |
134 | public boolean createFile(String path, Bitmap bitmap) {
135 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
136 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
137 | byte[] byteArray = stream.toByteArray();
138 | return createFile(path, byteArray);
139 | }
140 |
141 | public boolean deleteFile(String path) {
142 | File file = new File(path);
143 | return file.delete();
144 | }
145 |
146 | public boolean isFileExist(String path) {
147 | return new File(path).exists();
148 | }
149 |
150 | public byte[] readFile(String path) {
151 | final FileInputStream stream;
152 | try {
153 | stream = new FileInputStream(new File(path));
154 | return readFile(stream);
155 | } catch (FileNotFoundException e) {
156 | Log.e(TAG, "Failed to read file to input stream", e);
157 | return null;
158 | }
159 | }
160 |
161 | public String readTextFile(String path) {
162 | byte[] bytes = readFile(path);
163 | return new String(bytes);
164 | }
165 |
166 | public void appendFile(String path, String content) {
167 | appendFile(path, content.getBytes());
168 | }
169 |
170 | public void appendFile(String path, byte[] bytes) {
171 | if (!isFileExist(path)) {
172 | Log.w(TAG, "Impossible to append content, because such file doesn't exist");
173 | return;
174 | }
175 |
176 | try {
177 | FileOutputStream stream = new FileOutputStream(new File(path), true);
178 | stream.write(bytes);
179 | stream.write(System.getProperty("line.separator").getBytes());
180 | stream.flush();
181 | stream.close();
182 | } catch (IOException e) {
183 | Log.e(TAG, "Failed to append content to file", e);
184 | }
185 | }
186 |
187 | public List
365 | *
366 | * @param content The content to encrypt or descrypt.
367 | * @param encryptionMode Use: {@link Cipher#ENCRYPT_MODE} or
368 | * {@link Cipher#DECRYPT_MODE}
369 | * @return
370 | */
371 | private synchronized byte[] encrypt(byte[] content, int encryptionMode) {
372 | final byte[] secretKey = mConfiguration.getSecretKey();
373 | final byte[] ivx = mConfiguration.getIvParameter();
374 | return SecurityUtil.encrypt(content, encryptionMode, secretKey, ivx);
375 | }
376 |
377 | /**
378 | * Delete the directory and all sub content.
379 | *
380 | * @param path The absolute directory path. For example:
381 | * mnt/sdcard/NewFolder/.
382 | * @return True
if the directory was deleted, otherwise return
383 | * False
384 | */
385 | private boolean deleteDirectoryImpl(String path) {
386 | File directory = new File(path);
387 |
388 | // If the directory exists then delete
389 | if (directory.exists()) {
390 | File[] files = directory.listFiles();
391 | if (files == null) {
392 | return true;
393 | }
394 | // Run on all sub files and folders and delete them
395 | for (int i = 0; i < files.length; i++) {
396 | if (files[i].isDirectory()) {
397 | deleteDirectoryImpl(files[i].getAbsolutePath());
398 | } else {
399 | files[i].delete();
400 | }
401 | }
402 | }
403 | return directory.delete();
404 | }
405 |
406 | /**
407 | * Get all files under the directory
408 | *
409 | * @param directory
410 | * @param out
411 | * @return
412 | */
413 | private void getDirectoryFilesImpl(File directory, List
8 | */
9 | public class ImmutablePair
9 | *
10 | *
6 | *
7 | *
8 | * Every implementation of the Java platform is required to support the
9 | * following standard Cipher transformations with the keysizes in parentheses:
10 | *
11 | *
27 | *
28 | * @author Roman Kushnarenko - sromku (sromku@gmail.com)
29 | *
30 | */
31 | public class CipherTransformationType {
32 | private static final String _ = "/";
33 |
34 | public static final String AES_CBC_NoPadding = CipherAlgorithmType.AES + _ + CipherModeType.CBC + _ + CipherPaddingType.NoPadding;
35 | public static final String AES_CBC_PKCS5Padding = CipherAlgorithmType.AES + _ + CipherModeType.CBC + _ + CipherPaddingType.PKCS5Padding;
36 | public static final String AES_ECB_NoPadding = CipherAlgorithmType.AES + _ + CipherModeType.ECB + _ + CipherPaddingType.NoPadding;
37 | public static final String AES_ECB_PKCS5Padding = CipherAlgorithmType.AES + _ + CipherModeType.ECB + _ + CipherPaddingType.PKCS5Padding;
38 |
39 | public static final String DES_CBC_NoPadding = CipherAlgorithmType.DES + _ + CipherModeType.CBC + _ + CipherPaddingType.NoPadding;
40 | public static final String DES_CBC_PKCS5Padding = CipherAlgorithmType.DES + _ + CipherModeType.CBC + _ + CipherPaddingType.PKCS5Padding;
41 | public static final String DES_ECB_NoPadding = CipherAlgorithmType.DES + _ + CipherModeType.ECB + _ + CipherPaddingType.NoPadding;
42 | public static final String DES_ECB_PKCS5Padding = CipherAlgorithmType.DES + _ + CipherModeType.ECB + _ + CipherPaddingType.PKCS5Padding;
43 |
44 | public static final String DESede_CBC_NoPadding = CipherAlgorithmType.DESede + _ + CipherModeType.CBC + _ + CipherPaddingType.NoPadding;
45 | public static final String DESede_CBC_PKCS5Padding = CipherAlgorithmType.DESede + _ + CipherModeType.CBC + _ + CipherPaddingType.PKCS5Padding;
46 | public static final String DESede_ECB_NoPadding = CipherAlgorithmType.DESede + _ + CipherModeType.ECB + _ + CipherPaddingType.NoPadding;
47 | public static final String DESede_ECB_PKCS5Padding = CipherAlgorithmType.DESede + _ + CipherModeType.ECB + _ + CipherPaddingType.PKCS5Padding;
48 |
49 | public static final String RSA_ECB_PKCS1Padding = CipherAlgorithmType.RSA + _ + CipherModeType.ECB + _ + CipherPaddingType.PKCS1Padding;
50 | public static final String RSA_ECB_OAEPWithSHA_1AndMGF1Padding = CipherAlgorithmType.RSA + _ + CipherModeType.ECB + _ + CipherPaddingType.OAEPWithSHA_1AndMGF1Padding;
51 | public static final String RSA_ECB_OAEPWithSHA_256AndMGF1Padding = CipherAlgorithmType.RSA + _ + CipherModeType.ECB + _ + CipherPaddingType.OAEPWithSHA_256AndMGF1Padding;
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/storage/src/main/java/com/snatik/storage/security/SecurityUtil.java:
--------------------------------------------------------------------------------
1 | package com.snatik.storage.security;
2 |
3 | import android.util.Log;
4 |
5 | import java.io.UnsupportedEncodingException;
6 | import java.security.InvalidAlgorithmParameterException;
7 | import java.security.InvalidKeyException;
8 | import java.security.NoSuchAlgorithmException;
9 |
10 | import javax.crypto.BadPaddingException;
11 | import javax.crypto.Cipher;
12 | import javax.crypto.IllegalBlockSizeException;
13 | import javax.crypto.NoSuchPaddingException;
14 | import javax.crypto.SecretKey;
15 | import javax.crypto.spec.IvParameterSpec;
16 | import javax.crypto.spec.SecretKeySpec;
17 |
18 | /**
19 | * Security utils for encryption, xor and more
20 | *
21 | * @author Roman Kushnarenko - sromku (sromku@gmail.com)
22 | */
23 | public class SecurityUtil {
24 |
25 | private static final String TAG = "SecurityUtil";
26 |
27 | /**
28 | * Encrypt or Descrypt the content.
29 | *
30 | * @param content The content to encrypt or descrypt.
31 | * @param encryptionMode Use: {@link Cipher#ENCRYPT_MODE} or
32 | * {@link Cipher#DECRYPT_MODE}
33 | * @param secretKey Set the secret key for encryption of file content.
34 | * Important: The length must be 16 long. Uses SHA-256
35 | * to generate a hash from your key and trim the result to 128
36 | * bit (16 bytes)
37 | * @param ivx This is not have to be secret. It used just for better
38 | * randomizing the cipher. You have to use the same IV parameter
39 | * within the same encrypted and written files. Means, if you
40 | * want to have the same content after descryption then the same
41 | * IV must be used. About this parameter from wiki:
42 | * https://en.wikipedia.org/wiki/Block_cipher_modes_of_operation
43 | * #Initialization_vector_.28IV.29 Important: The length
44 | * must be 16 long
45 | * @return
46 | */
47 | public static byte[] encrypt(byte[] content, int encryptionMode, final byte[] secretKey, final byte[] ivx) {
48 | if (secretKey.length != 16 || ivx.length != 16) {
49 | Log.w(TAG, "Set the encryption parameters correctly. The must be 16 length long each");
50 | return null;
51 | }
52 |
53 | try {
54 | SecretKey secretkey = new SecretKeySpec(secretKey, CipherAlgorithmType.AES.getAlgorithmName());
55 | IvParameterSpec IV = new IvParameterSpec(ivx);
56 | String transformation = CipherTransformationType.AES_CBC_PKCS5Padding;
57 | Cipher decipher = Cipher.getInstance(transformation);
58 | decipher.init(encryptionMode, secretkey, IV);
59 | return decipher.doFinal(content);
60 | } catch (NoSuchAlgorithmException e) {
61 | Log.e(TAG, "Failed to encrypt/descrypt - Unknown Algorithm", e);
62 | return null;
63 | } catch (NoSuchPaddingException e) {
64 | Log.e(TAG, "Failed to encrypt/descrypt- Unknown Padding", e);
65 | return null;
66 | } catch (InvalidKeyException e) {
67 | Log.e(TAG, "Failed to encrypt/descrypt - Invalid Key", e);
68 | return null;
69 | } catch (InvalidAlgorithmParameterException e) {
70 | Log.e(TAG, "Failed to encrypt/descrypt - Invalid Algorithm Parameter", e);
71 | return null;
72 | } catch (IllegalBlockSizeException e) {
73 | Log.e(TAG, "Failed to encrypt/descrypt", e);
74 | return null;
75 | } catch (BadPaddingException e) {
76 | Log.e(TAG, "Failed to encrypt/descrypt", e);
77 | return null;
78 | }
79 | }
80 |
81 | /**
82 | * Do xor operation on the string with the key
83 | *
84 | * @param msg The string to xor on
85 | * @param key The key by which the xor will work
86 | * @return The string after xor
87 | */
88 | public String xor(String msg, String key) {
89 | try {
90 | final String UTF_8 = "UTF-8";
91 | byte[] msgArray;
92 |
93 | msgArray = msg.getBytes(UTF_8);
94 |
95 | byte[] keyArray = key.getBytes(UTF_8);
96 |
97 | byte[] out = new byte[msgArray.length];
98 | for (int i = 0; i < msgArray.length; i++) {
99 | out[i] = (byte) (msgArray[i] ^ keyArray[i % keyArray.length]);
100 | }
101 | return new String(out, UTF_8);
102 | } catch (UnsupportedEncodingException e) {
103 | }
104 | return null;
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------