使用 Xamarin Forms 创建离线移动密码管理器






4.39/5 (9投票s)
或将旧手机转换为密码管理器。
引言
我使用 Xamarin 编写 iOS 应用程序已有一段时间,一直想尝试 Xamarin Forms。我需要解决一个问题,这是我首次涉足 Xamarin Forms 领域。
问题
最近我在互联网上创建了一些需要使用复杂密码的新账户,而我现有的密码集已经不再足够/或勉强通过密码强度检查器。
我需要一个能够解决以下问题的方案:
a. 让我能够想出更安全的密码,以及
b. 足够容易记住
我的解决方案
多年来,记住一个密码并使用一套算法为所有网站生成密码的想法一直很流行。在看到这篇文章后,我真的很想采用相同的系统,问题是我总是忘记搜索词。
有一天,我在 Charles Leifer 的博客上看到了这篇文章,我们遇到了同样的问题,他为他的密码生成器创建了一个网络服务。
另一方面,我希望我的算法保持离线并由我控制,而且由于我有一个闲置的智能手机,我决定将其转换为我的特殊密码生成器,我将随身携带它。
背景
使用的技术:Xamarin Forms, .NetStandard
使用的技术:依赖注入(CodeProject 上有很多关于这个主题的优秀文章)
基本思路
这里的基本思想是将一个常见的、易于记忆的主字符串(例如,“Password”)转换为一个更复杂、更难破解、不可能记住的特定网站/服务的密码。
实现方法很简单,
- 将主字符串作为密钥,
- 添加特定的盐,我的惯例是使用网站名称
- 使用 SHA 加密
- 由于 SHA 返回所有字母和数字,我创建了一个所有可用字符的映射并使用该映射进行转换。
- 验证生成的密码是否符合要求。
- 添加额外字符以满足要求
代码结构
我的项目包含以下组件:
Core - 处理密码生成和密码方案的存储,它是一个 .NetStandard 库。
Core.Test - 单元测试,确保生成的密码一致并符合要求。
PasswordManager (Portable) - 为 Xamarin Forms 创建,从现在起我将其称为“PCL”。它是这个 Xamarin Forms 应用程序的骨干。Android 和 iOS 项目都从 PCL 启动应用程序。
PasswordManager.Android - Android 项目
PasswordManager.iOS - iOS 项目,由于我没有 iOS 开发者订阅,请忽略。
将 PCL 转换为 .NetStandard 库
我过去一直在使用 Xamarin 编写 iOS,令我惊讶的是,我无法向 Xamarin Forms 可移植库添加任何框架 dll 引用,这是一个问题,因为它意味着我无法使用 System.Security.Cryptography 命名空间。
解决方案是什么?显然是使用 .NetStandard 库。这就是 Core 项目需要成为 .NetStandard 库的原因。
然而,我不能简单地将 .NetStandard 库添加回 PCL,它不会编译。
所以我们必须将 PCL 转换为目标 .NetStandard。
这涉及了这里概述的三个额外步骤。
- 使用 Nuget 从解决方案中移除 Xamarin Forms。
- 右键单击 PCL 并打开属性窗口。
- 目标 .NetStandard
- 选择至少 .Net Standard 1.3 以包含 Cryptography 命名空间。
- 应该已生成一个 project.json 文件,单击它进行编辑。
- 在 Frameworks 下,在 frameworks 下添加以下行。
"imports": "portable-net45+win8+wpa81+wp8"
所以文件看起来大概是这样的:
{ "supports": {}, "dependencies": { "Microsoft.NETCore.Portable.Compatibility": "1.0.1", "NETStandard.Library": "1.6.0", }, "frameworks": { "netstandard1.4": { "imports": "portable-net45+win8+wpa81+wp8" } }
7. 使用 Nuget,将 Xamarin Forms 添加回解决方案。
使用代码
密码生成
密码生成很简单,只需像这样创建 PasswordGenerator 的实例:
var passwordGen = new PasswordGenerator(PasswordRequirements.SymbolRequired | PasswordRequirements.UpperCaseRequired | PasswordRequirements.NumberRequired);
并通过调用来创建密码
var password = passwordGen.GeneratePassword(sourceKey, salt, minimumLength);
再次强调,这是 PasswordGenerator 采取的步骤:
- 获取主字符串,
- 添加特定的盐,我的惯例是使用网站名称
- 使用 SHA 加密
- 由于 SHA 返回所有字母和数字,但我需要使用符号,所以我创建了一个所有可用字符的映射并使用该映射进行转换。
- 验证生成的密码是否符合要求。
- 添加额外字符以满足未满足的要求。
存储密码方案
为什么?
现在我们有了一个使用不同选项生成的密码,我们如何记住这些选项是什么?一种选择是尝试所有组合。尝试做十次。记住,这里的目标是创建一个计算机难以猜测的密码。
所以我们需要以某种方式存储密码方案,按网站/服务存储密码的一个好处是,我们现在可以编写额外的逻辑来检查两个服务是否真的相同。
例如,我们不希望为“Facebook”与“facebook”生成不同的密码。
好的,怎么做?
对于存储,我选择了 Sql-net-PCL。
为了持久化我们的密码方案,我们首先需要创建数据库。
SqliteStorage.InitializeStorage(IoCProviders.GetProvider<IKnowStorage>().GetStoragePath(), databaseName);
然后我们可以使用以下方式存储适当的设置:
ServicePersistor.Persist(serviceName, requirement);
所以现在我们可以生成密码,存储用于生成密码的选项,并且我们可以使用以下方式检索密码方案:
var serviceEntity = ServicePersistor.GetPasswordSettings(serviceName);
简单,对吧?
IoCProviders 是什么?
任何人都会遇到的第一个挑战是数据库存储在哪里?
iOS 和 Android 有不同的文件系统,我需要每个平台给我一个可以用来存储数据库文件的位置。
由于 Core 项目旨在同时与 iOS 和 Android 配合使用,它不了解这些平台特定的复杂性。我怎么可能知道在哪里存储/查找这些文件?
这就是依赖注入发挥作用的地方,CodeProject 上有很多关于这方面的文章。
我的想法是,我可以在 Core 中定义一个接口,允许在特定平台项目中创建特定的实现细节,我可以使用这些实现来执行平台特定任务。
这里的 IoCProviders
是一个简单的字典,用于跟踪这些实现。
这是 IoCProviders
(命名很难)的全部荣耀:
public class IoCProviders
{
private static IDictionary<Type, object> _providers;
private static IDictionary<Type, object> Providers
{
get
{
if (_providers == null)
{
_providers = new Dictionary<Type, object>();
}
return _providers;
}
}
public static void Register<TType>(object provider)
{
Providers[typeof(TType)] = provider;
}
public static TType GetProvider<TType>()
{
return (TType)Providers[typeof(TType)];
}
}
那么移动端呢?
我已经有了生成密码的方法,而且我可以存储它,那么我如何实际使用它呢?
嗯,我确实承诺过一个应用程序,不是吗?
出于好奇,我选择了 Xamarin Forms。
说实话,我一开始并不太期待用 XAML 编写用户界面,但这次经历后我改变了主意。
应用程序的外观
应用程序的工作流程如下:
- 输入主密码
- 输入服务名称
- 确保设置正确
- 获取密码
- 为服务存储给定的密码方案
- 将密码复制到剪贴板(平台特定)
- 通知用户密码已复制(平台特定)
- 用户可以选择使用不同的设置生成新密码
(如果他们选择了相同的设置,将再次生成相同的密码)
以下是应用程序的外观:
代码呢?
我发现,一旦我创建了一个 ViewModel,XAML 提供了一种很好的方式来可视化控制流。
ViewModel = new PasswordRequestViewModel();
控制 UI 行为就变成了使用 PasswordRequestViewModel()
的属性进行设置。
以下是每个变量控制的带注释视图:
MasterPassword
– 主密码文本框的文本值ShowOrHideMasterPasswordTitle
– 显示按钮的标题ServiceName
– 服务名称文本框的文本值IncludeNumbersString
– 包含数字按钮的标题IncludeSymbolsString
– 包含符号按钮的标题IncludeUppercaseString
– 包含大写字母按钮的标题PasswordVisible
– 控制密码字段是否显示CreateNewPassword
– 控制生成新密码按钮的可见性,该按钮未在图中显示。
以下是其中一个属性的示例实现:
private string showOrHideMasterPasswordTitle;
public string ShowOrHideMasterPasswordTitle
{
get
{
return showOrHideMasterPasswordTitle;
}
set
{
if (showOrHideMasterPasswordTitle != value)
{
showOrHideMasterPasswordTitle = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ShowOrHideMasterPasswordTitle"));
}
}
}
在 XAML 中看起来是这样的:
<Button Text="{Binding ShowOrHideMasterPasswordTitle}" BackgroundColor="Transparent" Clicked="ShowHide_Clicked" />
我不会详细介绍 Clicked 事件,但那基本上是按钮的可见性和标题发生变化的地方。
等等,没有复选框吗?
这里的一个烦恼是 Xamarin Forms 没有内置的复选框/单选框,我想这是因为 iOS 不太喜欢它们。我采取了偷懒的方式,改用按钮。
例如,这是“包含大写字母”按钮的样子:
我会在点击事件中通过改变 IncludeUppercase
的值来更新文本。
private bool includeUppercase;
public bool IncludeUppercase
{
get { return includeUppercase; }
set
{
if (includeUppercase != value)
{
includeUppercase = value;
IncludeUppercaseString = value ? "[x] Include Upper case" : "[ ] Include Upper case";
}
}
}
这里的 IncludeUppercase
是真实的值,而 IncludeUppercaseString
仅用于 UI。
平台特定实现
我谈到了平台特定的实现,它们实际上是无法在通用代码中编写的简单事物。
例如,iOS 和 Android 有自己的处理剪贴板的 API,Android 有Toast,iOS 有本地通知。
这就是我拥有以下接口的原因:
public interface ICopyToClipboard
{
void CopyToClipboard(string value);
}
public interface IKnowStorage
{
string GetStoragePath();
}
public interface IProvideNotifications
{
void Notify(string title, string message);
}
实现很简单。对于 Android:
public class ClipboardProvider : ICopyToClipboard
{
Context FormContext;
public ClipboardProvider(Context context)
{
FormContext = context;
}
public void CopyToClipboard(string value)
{
ClipboardManager clipboard = (ClipboardManager)FormContext.GetSystemService(Context.ClipboardService);
ClipData clip = ClipData.NewPlainText("Clipboard", value);
clipboard.PrimaryClip = clip;
}
}
public class NotificationProvider : IProvideNotifications
{
Context FormContext;
public NotificationProvider(Context context)
{
FormContext = context;
}
public void Notify(string title, string message)
{
Toast.MakeText(FormContext, message, ToastLength.Short).Show();
}
}
public class StorageProvider : IKnowStorage
{
public string GetStoragePath()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Personal);
}
}
这些实现被传递到应用程序中,并且应用程序使用 IoCProviders 注册它们。
public App
(
IKnowStorage storageProvider,
ICopyToClipboard clipboardProvider,
IProvideNotifications notificationProvider
)
{
InitializeComponent();
IoCProviders.Register<IKnowStorage>(storageProvider);
IoCProviders.Register<ICopyToClipboard>(clipboardProvider);
IoCProviders.Register<IProvideNotifications>(notificationProvider);
MainPage = new PasswordManager.MainPage();
}
最后一件事
当你的应用在后台运行时,如果一个陌生人走过来,你可能不希望那个人打开应用并看到你的主密码。这就是为什么我总是在应用恢复时清除密码和主密码。
Xamarin Forms 提供了一个很好的方法来重写:
protected override void OnResume() { // Handle when your app resumes ((PasswordManager.MainPage)MainPage).ClearEnteredData(); }
所以回顾一下,移动应用代码的工作原理如下:
- 创建特定平台的存储、通知和剪贴板提供程序实例。
- 使用 IoCProviders 注册提供程序。
- 使用 MainPage 启动应用程序。
- XAML 文件描述了应用程序的布局,它定义了标签、文本和事件。
- 控件属性,如标题、可见性,绑定到 PasswordRequestViewModel 中的变量。
- 点击事件切换控件的标题、可见性和文本值。
关注点
对我来说,真正的大问题是,这安全吗?我怎样才能让它真正安全?
我想为每个主密码创建一个不同的数据库(当然文件名是加密的),但我觉得那会为潜在的攻击者留下至少一点痕迹,我不喜欢那样。
所以现在我用同一个数据库来处理所有事情,不留下任何痕迹。如果我需要一个不同的密码,我总可以向主密码追加一些东西。
另一方面,他们也必须找出我的用户名,也许这是不使用同一组用户名在每个网站上的一个动机?
这对我来说真的是一个实验项目,我预计每个人对安全密码都会有稍微不同的想法(例如,密码的长度可能可以调整)。请随意调整以符合您的喜好,事实上我鼓励这样做。我非常喜欢针对特定需求进行调整,而不是创建试图让所有人都满意的通用解决方案。
致谢
我要感谢所有在 Code Project 上撰写这些精彩文章的人。我在职业生涯早期从这些文章中学到了很多,这次我终于找到了值得写的东西。
特别感谢Charles Leifer,我非常喜欢你的周六项目想法,它真的鼓励我尝试做类似的事情。
源代码
历史
2017 年 6 月 17 日 - 清理了语言
2017 年 5 月 22 日 – 初稿