├── README.md └── SecuritySharedPreference ├── .gitignore ├── app ├── .gitignore ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── domain │ │ └── securitysharedpreference │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── domain │ │ │ └── securitysharedpreference │ │ │ ├── EncryptUtil.java │ │ │ ├── LoginActivity.java │ │ │ └── SecuritySharedPreference.java │ └── res │ │ ├── layout │ │ └── activity_login.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── domain │ └── securitysharedpreference │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /README.md: -------------------------------------------------------------------------------- 1 | # SecuritySharedPreference 2 | Android Security SharedPreference(Android安全的SharedPreference存储方式)欢迎Star,我的博客:http://blog.csdn.net/voidmain_123/article/details/53338393 3 | 4 | ## 前言 ## 5 | 安全问题长久以来就是Android系统的一大弊病,很多人也因此舍弃Android选择了苹果,作为一个Android Developer,我们需要对用户的隐私负责,需要对用户的数据安全倾尽全力,想到这里,我就热血沸腾,仿佛自己化身正义的天使(我编不下去了。。。)。 6 | 7 | ## 概述 ## 8 | 现在,我们回归正题,SharedPreference是我们比较常用的保存数据到本地的方式,我们习惯在SharedPreference中保存用户信息,用户偏好设置,以及记住用户名密码等等。但是,SharedPreference存在一些安全隐患,我们都知道,SharedPreference是以“键值对”的形式把数据保存在data/data/packageName/shared_prefs文件夹中的xml文件中。 9 | 在正常的情况下,我们没办法访问data/data目录,我们也没办法拿到xml中的文件。但是,我们root手机之后,通过命令行可以获得读写data/data目录的权限,我们SharedPreference中保存的数据也就很容易泄漏,造成不可挽回的损失。那么我们今天就从SharedPreference开刀,为APP的安全尽一份绵薄之力。 10 | 11 | 这样我们基本上实现了SharedPreference的加解密存储,APP的数据安全进一步得到了保证,现在和大家说一下使用方式:首先我们看一下普通的SharedPreference: 12 | ``` 13 | /** 14 | * 以常规的SharedPreference保存数据 15 | */ 16 | private void saveInCommonPreference(){ 17 | SharedPreferences sharedPreferences = getSharedPreferences("common_prefs", Context.MODE_PRIVATE); 18 | SharedPreferences.Editor editor = sharedPreferences.edit(); 19 | editor.putString("username", mEmailView.getText().toString()); 20 | editor.putString("password", mPasswordView.getText().toString()); 21 | editor.apply(); 22 | } 23 | ``` 24 | 我们看一下本地保存的效果 25 | ``` 26 | 27 | 28 | 1136138123@qq.com 29 | 147258369 30 | 31 | ``` 32 | 其次,我们来看一下SecuritySharedPreference的使用方式: 33 | ``` 34 | /** 35 | * 以加密的SharedPreference保存数据 36 | */ 37 | private void saveInSecurityPreference(){ 38 | SecuritySharedPreference securitySharedPreference = new SecuritySharedPreference(getApplicationContext(), "security_prefs", Context.MODE_PRIVATE); 39 | SecuritySharedPreference.SecurityEditor securityEditor = securitySharedPreference.edit(); 40 | securityEditor.putString("username", mEmailView.getText().toString()); 41 | securityEditor.putString("password", mPasswordView.getText().toString()); 42 | securityEditor.apply(); 43 | } 44 | ``` 45 | 我们看一下本地保存的效果有什么区别 46 | ``` 47 | 48 | 49 | Rnfpxffj9rNl29dsoQxlUzpSaR9m5K6myIYtqQOiIRU= 50 | HoHo+CFJrXK3CPMUpcTTow== 51 | 52 | 53 | ``` 54 | 效果非常棒!我们通过简简单单的两个类,实现了SharedPreference的加密。虽然只是个非常简单的小功能,但是给数据安全提供了护盾,O(∩_∩)O哈哈~ 55 | 有的同学可能会说,我的项目中已经使用了SharedPreference,如何迁移到SecuritySharedPreference呢?如何进行不加密数据到加密数据的过渡呢?这一点其实我已经替大家做好了,我们在下一次升级应用的时候,在第一次使用SharedPreference时,调用handleTransition()方法进行数据加密的过渡。 56 | -------------------------------------------------------------------------------- /SecuritySharedPreference/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jianma/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/src/androidTest/java/com/domain/securitysharedpreference/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.domain.securitysharedpreference; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.domain.securitysharedpreference", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/src/main/java/com/domain/securitysharedpreference/EncryptUtil.java: -------------------------------------------------------------------------------- 1 | package com.domain.securitysharedpreference; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.provider.Settings; 7 | import android.text.TextUtils; 8 | import android.util.Base64; 9 | import android.util.Log; 10 | 11 | import java.security.MessageDigest; 12 | import java.security.NoSuchAlgorithmException; 13 | 14 | import javax.crypto.Cipher; 15 | import javax.crypto.spec.SecretKeySpec; 16 | 17 | /** 18 | * AES加密解密工具 19 | * @author Max 20 | * 2016年11月25日15:25:17 21 | */ 22 | public class EncryptUtil { 23 | 24 | private String key; 25 | private static EncryptUtil instance; 26 | private static final String TAG = EncryptUtil.class.getSimpleName(); 27 | 28 | 29 | private EncryptUtil(Context context){ 30 | String serialNo = getDeviceSerialNumber(context); 31 | //加密随机字符串生成AES key 32 | key = SHA(serialNo + "#$ERDTS$D%F^Gojikbh").substring(0, 16); 33 | Log.e(TAG, key); 34 | } 35 | 36 | /** 37 | * 单例模式 38 | * @param context context 39 | * @return 40 | */ 41 | public static EncryptUtil getInstance(Context context){ 42 | if (instance == null){ 43 | synchronized (EncryptUtil.class){ 44 | if (instance == null){ 45 | instance = new EncryptUtil(context); 46 | } 47 | } 48 | } 49 | 50 | return instance; 51 | } 52 | 53 | /** 54 | * Gets the hardware serial number of this device. 55 | * 56 | * @return serial number or Settings.Secure.ANDROID_ID if not available. 57 | */ 58 | @SuppressLint("HardwareIds") 59 | private String getDeviceSerialNumber(Context context) { 60 | // We're using the Reflection API because Build.SERIAL is only available 61 | // since API Level 9 (Gingerbread, Android 2.3). 62 | try { 63 | String deviceSerial = (String) Build.class.getField("SERIAL").get(null); 64 | if (TextUtils.isEmpty(deviceSerial)) { 65 | return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 66 | } else { 67 | return deviceSerial; 68 | } 69 | } catch (Exception ignored) { 70 | // Fall back to Android_ID 71 | return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 72 | } 73 | } 74 | 75 | 76 | /** 77 | * SHA加密 78 | * @param strText 明文 79 | * @return 80 | */ 81 | private String SHA(final String strText){ 82 | // 返回值 83 | String strResult = null; 84 | // 是否是有效字符串 85 | if (strText != null && strText.length() > 0){ 86 | try{ 87 | // SHA 加密开始 88 | MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 89 | // 传入要加密的字符串 90 | messageDigest.update(strText.getBytes()); 91 | byte byteBuffer[] = messageDigest.digest(); 92 | StringBuffer strHexString = new StringBuffer(); 93 | for (int i = 0; i < byteBuffer.length; i++){ 94 | String hex = Integer.toHexString(0xff & byteBuffer[i]); 95 | if (hex.length() == 1){ 96 | strHexString.append('0'); 97 | } 98 | strHexString.append(hex); 99 | } 100 | strResult = strHexString.toString(); 101 | } catch (NoSuchAlgorithmException e) { 102 | e.printStackTrace(); 103 | } 104 | } 105 | 106 | return strResult; 107 | } 108 | 109 | 110 | /** 111 | * AES128加密 112 | * @param plainText 明文 113 | * @return 114 | */ 115 | public String encrypt(String plainText) { 116 | try { 117 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 118 | SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); 119 | cipher.init(Cipher.ENCRYPT_MODE, keyspec); 120 | byte[] encrypted = cipher.doFinal(plainText.getBytes()); 121 | return Base64.encodeToString(encrypted, Base64.NO_WRAP); 122 | } catch (Exception e) { 123 | e.printStackTrace(); 124 | return null; 125 | } 126 | } 127 | 128 | /** 129 | * AES128解密 130 | * @param cipherText 密文 131 | * @return 132 | */ 133 | public String decrypt(String cipherText) { 134 | try { 135 | byte[] encrypted1 = Base64.decode(cipherText, Base64.NO_WRAP); 136 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 137 | SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); 138 | cipher.init(Cipher.DECRYPT_MODE, keyspec); 139 | byte[] original = cipher.doFinal(encrypted1); 140 | String originalString = new String(original); 141 | return originalString; 142 | } catch (Exception e) { 143 | e.printStackTrace(); 144 | return null; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/src/main/java/com/domain/securitysharedpreference/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package com.domain.securitysharedpreference; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | import android.view.View.OnClickListener; 9 | import android.widget.AutoCompleteTextView; 10 | import android.widget.Button; 11 | import android.widget.EditText; 12 | 13 | /** 14 | * A login screen that offers login via email/password. 15 | */ 16 | public class LoginActivity extends AppCompatActivity { 17 | 18 | private AutoCompleteTextView mEmailView; 19 | private EditText mPasswordView; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_login); 25 | mEmailView = (AutoCompleteTextView) findViewById(R.id.email); 26 | mPasswordView = (EditText) findViewById(R.id.password); 27 | //常规方式保存用户名密码 28 | Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); 29 | mEmailSignInButton.setOnClickListener(new OnClickListener() { 30 | @Override 31 | public void onClick(View view) { 32 | saveInCommonPreference(); 33 | } 34 | }); 35 | 36 | //加密方式保存用户名密码 37 | Button mEmailSignInButtonSecurity = (Button) findViewById(R.id.email_sign_in_button_secure); 38 | mEmailSignInButtonSecurity.setOnClickListener(new OnClickListener() { 39 | @Override 40 | public void onClick(View v) { 41 | saveInSecurityPreference(); 42 | } 43 | }); 44 | } 45 | 46 | /** 47 | * 以常规的SharedPreference保存数据 48 | */ 49 | private void saveInCommonPreference(){ 50 | SharedPreferences sharedPreferences = getSharedPreferences("common_prefs", Context.MODE_PRIVATE); 51 | SharedPreferences.Editor editor = sharedPreferences.edit(); 52 | editor.putString("username", mEmailView.getText().toString()); 53 | editor.putString("password", mPasswordView.getText().toString()); 54 | editor.apply(); 55 | } 56 | 57 | /** 58 | * 以加密的SharedPreference保存数据 59 | */ 60 | private void saveInSecurityPreference(){ 61 | SecuritySharedPreference securitySharedPreference = new SecuritySharedPreference(getApplicationContext(), "security_prefs", Context.MODE_PRIVATE); 62 | SecuritySharedPreference.SecurityEditor securityEditor = securitySharedPreference.edit(); 63 | securityEditor.putString("username", mEmailView.getText().toString()); 64 | securityEditor.putString("password", mPasswordView.getText().toString()); 65 | securityEditor.apply(); 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/src/main/java/com/domain/securitysharedpreference/SecuritySharedPreference.java: -------------------------------------------------------------------------------- 1 | package com.domain.securitysharedpreference; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.os.Build; 7 | import android.preference.PreferenceManager; 8 | import android.support.annotation.Nullable; 9 | import android.text.TextUtils; 10 | import android.util.Log; 11 | 12 | import java.util.HashMap; 13 | import java.util.HashSet; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | /** 18 | * 自动加密SharedPreference 19 | * Created by Max on 2016/11/23. 20 | */ 21 | 22 | public class SecuritySharedPreference implements SharedPreferences { 23 | 24 | private SharedPreferences mSharedPreferences; 25 | private static final String TAG = SecuritySharedPreference.class.getName(); 26 | private Context mContext; 27 | 28 | /** 29 | * constructor 30 | * @param context should be ApplicationContext not activity 31 | * @param name file name 32 | * @param mode context mode 33 | */ 34 | public SecuritySharedPreference(Context context, String name, int mode){ 35 | mContext = context; 36 | if (TextUtils.isEmpty(name)){ 37 | mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 38 | } else { 39 | mSharedPreferences = context.getSharedPreferences(name, mode); 40 | } 41 | 42 | } 43 | 44 | @Override 45 | public Map getAll() { 46 | final Map encryptMap = mSharedPreferences.getAll(); 47 | final Map decryptMap = new HashMap<>(); 48 | for (Map.Entry entry : encryptMap.entrySet()){ 49 | Object cipherText = entry.getValue(); 50 | if (cipherText != null){ 51 | decryptMap.put(entry.getKey(), entry.getValue().toString()); 52 | } 53 | } 54 | return decryptMap; 55 | } 56 | 57 | /** 58 | * encrypt function 59 | * @return cipherText base64 60 | */ 61 | private String encryptPreference(String plainText){ 62 | return EncryptUtil.getInstance(mContext).encrypt(plainText); 63 | } 64 | 65 | /** 66 | * decrypt function 67 | * @return plainText 68 | */ 69 | private String decryptPreference(String cipherText){ 70 | return EncryptUtil.getInstance(mContext).decrypt(cipherText); 71 | } 72 | 73 | @Nullable 74 | @Override 75 | public String getString(String key, String defValue) { 76 | final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null); 77 | return encryptValue == null ? defValue : decryptPreference(encryptValue); 78 | } 79 | 80 | @Nullable 81 | @Override 82 | public Set getStringSet(String key, Set defValues) { 83 | final Set encryptSet = mSharedPreferences.getStringSet(encryptPreference(key), null); 84 | if (encryptSet == null){ 85 | return defValues; 86 | } 87 | final Set decryptSet = new HashSet<>(); 88 | for (String encryptValue : encryptSet){ 89 | decryptSet.add(decryptPreference(encryptValue)); 90 | } 91 | return decryptSet; 92 | } 93 | 94 | @Override 95 | public int getInt(String key, int defValue) { 96 | final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null); 97 | if (encryptValue == null) { 98 | return defValue; 99 | } 100 | return Integer.parseInt(decryptPreference(encryptValue)); 101 | } 102 | 103 | @Override 104 | public long getLong(String key, long defValue) { 105 | final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null); 106 | if (encryptValue == null) { 107 | return defValue; 108 | } 109 | return Long.parseLong(decryptPreference(encryptValue)); 110 | } 111 | 112 | @Override 113 | public float getFloat(String key, float defValue) { 114 | final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null); 115 | if (encryptValue == null) { 116 | return defValue; 117 | } 118 | return Float.parseFloat(decryptPreference(encryptValue)); 119 | } 120 | 121 | @Override 122 | public boolean getBoolean(String key, boolean defValue) { 123 | final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null); 124 | if (encryptValue == null) { 125 | return defValue; 126 | } 127 | return Boolean.parseBoolean(decryptPreference(encryptValue)); 128 | } 129 | 130 | @Override 131 | public boolean contains(String key) { 132 | return mSharedPreferences.contains(encryptPreference(key)); 133 | } 134 | 135 | @Override 136 | public SecurityEditor edit() { 137 | return new SecurityEditor(); 138 | } 139 | 140 | @Override 141 | public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 142 | mSharedPreferences.registerOnSharedPreferenceChangeListener(listener); 143 | } 144 | 145 | @Override 146 | public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 147 | mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); 148 | } 149 | 150 | /** 151 | * 处理加密过渡 152 | */ 153 | public void handleTransition(){ 154 | Map oldMap = mSharedPreferences.getAll(); 155 | Map newMap = new HashMap<>(); 156 | for (Map.Entry entry : oldMap.entrySet()){ 157 | Log.i(TAG, "key:"+entry.getKey()+", value:"+ entry.getValue()); 158 | newMap.put(encryptPreference(entry.getKey()), encryptPreference(entry.getValue().toString())); 159 | } 160 | Editor editor = mSharedPreferences.edit(); 161 | editor.clear().commit(); 162 | for (Map.Entry entry : newMap.entrySet()){ 163 | editor.putString(entry.getKey(), entry.getValue()); 164 | } 165 | editor.commit(); 166 | } 167 | 168 | /** 169 | * 自动加密Editor 170 | */ 171 | final class SecurityEditor implements Editor { 172 | 173 | private Editor mEditor; 174 | 175 | /** 176 | * constructor 177 | */ 178 | private SecurityEditor(){ 179 | mEditor = mSharedPreferences.edit(); 180 | } 181 | 182 | @Override 183 | public Editor putString(String key, String value) { 184 | mEditor.putString(encryptPreference(key), encryptPreference(value)); 185 | return this; 186 | } 187 | 188 | @Override 189 | public Editor putStringSet(String key, Set values) { 190 | final Set encryptSet = new HashSet<>(); 191 | for (String value : values){ 192 | encryptSet.add(encryptPreference(value)); 193 | } 194 | mEditor.putStringSet(encryptPreference(key), encryptSet); 195 | return this; 196 | } 197 | 198 | @Override 199 | public Editor putInt(String key, int value) { 200 | mEditor.putString(encryptPreference(key), encryptPreference(Integer.toString(value))); 201 | return this; 202 | } 203 | 204 | @Override 205 | public Editor putLong(String key, long value) { 206 | mEditor.putString(encryptPreference(key), encryptPreference(Long.toString(value))); 207 | return this; 208 | } 209 | 210 | @Override 211 | public Editor putFloat(String key, float value) { 212 | mEditor.putString(encryptPreference(key), encryptPreference(Float.toString(value))); 213 | return this; 214 | } 215 | 216 | @Override 217 | public Editor putBoolean(String key, boolean value) { 218 | mEditor.putString(encryptPreference(key), encryptPreference(Boolean.toString(value))); 219 | return this; 220 | } 221 | 222 | @Override 223 | public Editor remove(String key) { 224 | mEditor.remove(encryptPreference(key)); 225 | return this; 226 | } 227 | 228 | /** 229 | * Mark in the editor to remove all values from the preferences. 230 | * @return this 231 | */ 232 | @Override 233 | public Editor clear() { 234 | mEditor.clear(); 235 | return this; 236 | } 237 | 238 | /** 239 | * 提交数据到本地 240 | * @return Boolean 判断是否提交成功 241 | */ 242 | @Override 243 | public boolean commit() { 244 | 245 | return mEditor.commit(); 246 | } 247 | 248 | /** 249 | * Unlike commit(), which writes its preferences out to persistent storage synchronously, 250 | * apply() commits its changes to the in-memory SharedPreferences immediately but starts 251 | * an asynchronous commit to disk and you won't be notified of any failures. 252 | */ 253 | @Override 254 | @TargetApi(Build.VERSION_CODES.GINGERBREAD) 255 | public void apply() { 256 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 257 | mEditor.apply(); 258 | } else { 259 | commit(); 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /SecuritySharedPreference/app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 21 | 22 | 26 | 27 | 32 | 33 | 36 | 37 | 45 | 46 | 47 | 48 | 51 | 52 | 63 | 64 | 65 | 66 |