|
3 | 3 | using System; |
4 | 4 |
|
5 | 5 | #if __IOS__ |
| 6 | + |
6 | 7 | using Foundation; |
7 | 8 | using Security; |
| 9 | + |
| 10 | +#elif __ANDROID__ |
| 11 | + |
| 12 | +using System.Collections.Generic; |
| 13 | + |
| 14 | +using Java.IO; |
| 15 | +using Java.Security; |
| 16 | +using Javax.Crypto; |
| 17 | + |
| 18 | +using Android.Content; |
| 19 | + |
8 | 20 | #endif |
9 | 21 |
|
10 | 22 | namespace NomadCode.Azure |
@@ -56,43 +68,159 @@ bool saveItemToKeychain (string service, string account, string privateKey) |
56 | 68 |
|
57 | 69 | var success = status == SecStatusCode.Success; |
58 | 70 |
|
59 | | - if (!success) Console.WriteLine ($"Error in Keychain: {status}"); |
| 71 | + if (!success) |
| 72 | + { |
| 73 | + System.Diagnostics.Debug.WriteLine ($"Error in Keychain: {status}"); |
| 74 | + System.Diagnostics.Debug.WriteLine ($"If you are seeing error code '-34018' got to Project Options -> iOS Bundle Signing -> make sure Entitlements.plist is populated for Custom Entitlements for iPhoneSimulator configs"); |
| 75 | + } |
60 | 76 |
|
61 | 77 | return success; |
62 | 78 | } |
63 | 79 |
|
64 | 80 |
|
65 | | - void removeItemFromKeychain (string service) |
| 81 | + bool removeItemFromKeychain (string service) |
66 | 82 | { |
67 | 83 | var record = genericRecord (service); |
68 | 84 |
|
69 | | - SecKeyChain.Remove (record); |
| 85 | + var status = SecKeyChain.Remove (record); |
| 86 | + |
| 87 | + var success = status == SecStatusCode.Success; |
| 88 | + |
| 89 | + if (!success) |
| 90 | + { |
| 91 | + System.Diagnostics.Debug.WriteLine ($"Error in Keychain: {status}"); |
| 92 | + System.Diagnostics.Debug.WriteLine ($"If you are seeing error code '-34018' got to Project Options -> iOS Bundle Signing -> make sure Entitlements.plist is populated for Custom Entitlements for iPhoneSimulator configs"); |
| 93 | + } |
| 94 | + |
| 95 | + return success; |
70 | 96 | } |
71 | 97 |
|
72 | 98 | #else |
73 | 99 |
|
74 | | - // to implement, check out the links below: |
75 | | - // https://github.com/sameerkapps/SecureStorage/blob/master/SecureStorage/Plugin.SecureStorage.Android/SecureStorageImplementation.cs |
76 | | - // https://github.com/xamarin/Xamarin.Auth/blob/portable-bait-and-switch/source/Xamarin.Auth.XamarinAndroid/PlatformSpecific/AndroidAccountStore.cs |
77 | | - // https://github.com/xamarin/Xamarin.Auth/blob/portable-bait-and-switch/source/Xamarin.Auth.XamarinAndroid/PlatformSpecific/AndroidAccountStore.Async.cs |
| 100 | + static Dictionary<string, KeyStore> keyStoresCache = new Dictionary<string, KeyStore> (); |
| 101 | + |
| 102 | + |
| 103 | + KeyStore getKeystore (string service) |
| 104 | + { |
| 105 | + var context = Android.App.Application.Context; |
| 106 | + |
| 107 | + KeyStore keystore; |
| 108 | + |
| 109 | + var serviceId = $"{context.PackageName}-{service}"; |
| 110 | + |
| 111 | + if (keyStoresCache.TryGetValue (serviceId, out keystore)) |
| 112 | + { |
| 113 | + return keystore; |
| 114 | + } |
| 115 | + |
| 116 | + var password = service.ToCharArray (); |
| 117 | + |
| 118 | + keystore = KeyStore.GetInstance (KeyStore.DefaultType); |
| 119 | + |
| 120 | + // var protection = new KeyStore.PasswordProtection (password); |
| 121 | + |
| 122 | + try |
| 123 | + { |
| 124 | + // TODO: this isn't right, fix it |
| 125 | + using (var stream = context.OpenFileInput (serviceId)) |
| 126 | + { |
| 127 | + keystore.Load (stream, password); |
| 128 | + } |
| 129 | + } |
| 130 | + catch (FileNotFoundException) |
| 131 | + { |
| 132 | + keystore.Load (null, password); |
| 133 | + } |
| 134 | + |
| 135 | + keyStoresCache [serviceId] = keystore; |
| 136 | + |
| 137 | + return keystore; |
| 138 | + } |
78 | 139 |
|
79 | 140 |
|
80 | 141 | Tuple<string, string> getItemFromKeychain (string service) |
81 | 142 | { |
82 | | - throw new NotImplementedException (); |
| 143 | + var context = Android.App.Application.Context; |
| 144 | + |
| 145 | + var password = service.ToCharArray (); |
| 146 | + |
| 147 | + var protection = new KeyStore.PasswordProtection (password); |
| 148 | + |
| 149 | + var keystore = getKeystore (service); |
| 150 | + |
| 151 | + var aliases = keystore.Aliases (); |
| 152 | + |
| 153 | + while (aliases.HasMoreElements) |
| 154 | + { |
| 155 | + var alias = aliases.NextElement ().ToString (); |
| 156 | + |
| 157 | + var item = keystore.GetEntry (alias, protection) as KeyStore.SecretKeyEntry; |
| 158 | + |
| 159 | + if (item != null) |
| 160 | + { |
| 161 | + var bytes = item.SecretKey.GetEncoded (); |
| 162 | + |
| 163 | + var serialized = System.Text.Encoding.UTF8.GetString (bytes); |
| 164 | + |
| 165 | + return new Tuple<string, string> (alias, serialized); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + return null; |
83 | 170 | } |
84 | 171 |
|
85 | 172 |
|
86 | 173 | bool saveItemToKeychain (string service, string account, string privateKey) |
87 | 174 | { |
88 | | - throw new NotImplementedException (); |
| 175 | + var context = Android.App.Application.Context; |
| 176 | + |
| 177 | + var password = service.ToCharArray (); |
| 178 | + |
| 179 | + var serviceId = $"{context.PackageName}-{service}"; |
| 180 | + |
| 181 | + var keystore = getKeystore (service); |
| 182 | + |
| 183 | + var item = new KeychainItem (privateKey); |
| 184 | + |
| 185 | + var secretEntry = new KeyStore.SecretKeyEntry (item); |
| 186 | + |
| 187 | + keystore.SetEntry (account, secretEntry, new KeyStore.PasswordProtection (password)); |
| 188 | + |
| 189 | + using (var stream = context.OpenFileOutput (serviceId, FileCreationMode.Private)) |
| 190 | + { |
| 191 | + keystore.Store (stream, password); |
| 192 | + } |
| 193 | + |
| 194 | + return true; |
89 | 195 | } |
90 | 196 |
|
91 | 197 |
|
92 | | - void removeItemFromKeychain (string service) |
| 198 | + bool removeItemFromKeychain (string service) |
93 | 199 | { |
94 | 200 | throw new NotImplementedException (); |
95 | 201 | } |
| 202 | + |
| 203 | + |
| 204 | + class KeychainItem : Java.Lang.Object, ISecretKey |
| 205 | + { |
| 206 | + const string raw = "RAW"; |
| 207 | + |
| 208 | + byte [] bytes; |
| 209 | + |
| 210 | + public KeychainItem (string data) |
| 211 | + { |
| 212 | + if (data == null) throw new ArgumentNullException (); |
| 213 | + |
| 214 | + bytes = System.Text.Encoding.UTF8.GetBytes (data); |
| 215 | + } |
| 216 | + |
| 217 | + public byte [] GetEncoded () => bytes; |
| 218 | + |
| 219 | + public string Algorithm => raw; |
| 220 | + |
| 221 | + public string Format => raw; |
| 222 | + } |
| 223 | + |
96 | 224 | #endif |
97 | 225 | } |
98 | 226 | } |
|
0 commit comments