Pages

Sunday, August 23, 2015

The Google Photos Management Problem(s)

The Executive Summary (you can pretend you’re an exec)

  • Storing photos in one place with one compelling interface is good. I wish Google had that.
  • Google Photos is bad.
  • Pictures on Google Drive is non-obvious.
  • Blogger integration with pictures is non-obvious.
  • I should be more careful.
  • Picasa is still required to make my blog go.
  • Google Photos is bad.
It’s not a mistake that I listed one item twice.

The Setup

Google strives, mostly successfully, to keep their applications simple. Simple to understand, simple to use, simple to explain. Google also wants us to store our millions of pictures with them, because they are a gateway to marketing, and thus advertising.

I have a blog, the very one you’re reading. It’s hosted on Blogger, which is owned by Google. I often use screen shots, a.k.a pictures, like this one.
Dewy grass sm
And, I use Windows Live Writer to compose my posts, because it has easy copy/paste of pictures, good formatting control, and useful plugins. (It’s also quite old and unmaintained, so here’s hoping Scott Hanselman is successful getting Microsoft to release it as open source, because I think WLW would rapidly improve.)

When I publish using WLW, the screen shots end up in a Google . . . somewhere. When I finish composing a blog post, WLW opens the post in my preferred browser, which is Google Chrome. It all looks hunky dory, and I’m pleased with my writing prowess, which I know is what chicks would dig if I weren’t a relatively old man.

The Story

So the other day I proudly opened my post about encrypting app.config in Microsoft’s new Edge browser (in Windows 10), and—what’s this? No images? Just gray boxes with a circle-dash icon?
image
Well, this new browser certainly does suck daikon. Good thing the pictures show in Internet Explorer, right? Nope. Same problem. But they look great in Chrome. Must be Windows 10. Or Windows Live Writer, which led me on an odyssey of reading about how Google “broke” blogging apps by requiring better authentication, and giving something like two years’ notice. Maybe that’s the problem? No, it isn’t, that got fixed fairly quickly.

Then it must be Windows 10. Somehow this got past the thousands of people testing the app for a year (as if). I’ll check in lovely Windows 7 from work.

Same thing. Only now, I don’t see pictures in Chrome, either. Which is the clue to the solution. Because at work, I hadn’t logged into my Google account before viewing my blog.

Ah ha. Maybe, somehow, the pictures aren’t public. And I never noticed, because I always viewed the blog while logged in. Bad on me for not testing other browsers. (I did test other browsers, though, in the past. I guess I unwittingly changed the permission.)

OK, I’ll just check the album and set it for public, like I would if it were in Google Drive. Now . . . where is it?

Google Photos? I’ve been hearing about that. It's the app on my phone I don't like. Yeah, there it is! All I have to do is . . . is . . . huh? This is your new, improved photo collecting and organizing application Mr Brin? This. Is. Bad.
  • Initial view is all your pictures, sorted descending by date, in every size imaginable (doubled, in my case, because of how WLW works).
  • Choose the Collections view and you get what, exactly? What’s a collection? It looks like an album. So why’s it called a collection? Oh, it’s because a collection could be photos, movies or stories (whatever those are). And a collection of photos is called—I’m perfectly serious—an album. While a collection of movies is called “Movies.”
    image
  • You can’t create an empty album. You have to create an album from existing pictures you’ve already uploaded. From that massive, date-sorted ocean you saw earlier. Which is the exact opposite of how a sane person creates an album. We, the people, create an album and then upload pictures from our phone or computer to that album. We at least want that option!
  • You can’t change the sharing of an album, as far as public/private is concerned. You can share to Google+, Facebook or Twitter, or get a shareable link.
More reading. I dimly recall Google is storing lots more stuff on Drive. Are they storing photos? Why, yes!
image

Excitedly, I click the link and, indeed, I see my photos. Some of them. And some are different from what’s in Photos, despite the icon and text telling me I’m looking at, um, Photos. No albums. Too bad, because it looks like I can make individual photos publically accessible here. This is an illusion though, as far as my blog is concerned. The settings aren’t related. So this is useless. Gamely, I check the settings, and indeed there’s an intriguing option.
image

But I don’t check it. It doesn’t feel right. Why would I need to connect Google Photos to Google Drive in order to access sharing properties on Google Albums that are, somehow, already set? And I’m nervous that, according to some descriptions, I can make changes in Drive that affect Photos, but not the other way around. Or is it the reverse?

The Solution

Wait . . . hang on. Isn’t Photos a new application? Weren’t we storing photos before? With some other program that I tried? (snap, snap, snap) Picasa. That’s it. Didn’t Picasa have albums? Is it possible? . . . Yes. Follow the trail.
image
image
image
image

Save and done. Now all of my blog pictures are visible again. That was easy.
image

The Problems

Did you keep count? Google has four places you can manage your pictures:
  1. photos.google.com, the new kid, where G wants us to end up.
  2. Google Photos accessible from Google Drive.
  3. Pictures in a folder in Google Drive, somehow connected to Google Photos.
  4. Pictures in Picasa.
It’s a mess. I’m sad.

Creating a Long-Lasting ClickOnce Certificate

This post’s for me, since I’ve occasionally had to look this up.

Here are the two command lines of code needed to create a long-lasting, untrusted ClickOnce certificate. You run this using the Visual Studio command prompt. I recommend putting the certificate info in a separate folder, documenting the procedure and passwords, and storing in version control. The purpose isn’t deep security, it’s letting the apps install and run with minimal fuss.
C:\MakeCertDemo> makecert -sv MyClickOnce.pvk -n "CN=MY ClickOnce" MyClickOnce.cer -b 08/01/2015 -e 12/31/2100 -r
C:\MakeCertDemo> pvk2pfx -pvk MyClickOnce.pvk -spc MyClickOnce.cer -pfx MyClickOnce.pfx -po X0PASS!

The –r switch is required to ensure the certificate’s length is greater that 1024 bits, otherwise Windows will refuse the ClickOnce installation. Older articles about using MakeCert omit that.

The development certificate created by Visual Studio when publishing a ClickOnce application lasts just a year. You can get with your network admin to create a domain-level certificate for internal publishing, if you have Windows Enterprise Server. Or buy a Verisign certificate for trusted public publishing. But often, small businesses just need to publish internal apps with little fanfare. The app developers need:
  1. A long-lasting certificate
  2. To not discover they don’t know the original certificate password (“Hey, who wrote this? Know password? OK, I’ll create a new certificate.”)
  3. Consistency

References

Certificate Expiration in ClickOnce Deployment

Stack Overflow

DigitallyCreated

Monday, August 10, 2015

More Encrypted Config Files–ClickOnce Complications

Prologue

In my previous post, I detailed a solution for encrypting sections of the app.config (the technique is similar in a web app). Since then, I ran into some issues in a Windows Form application I was developing.
  • I needed to encrypt email SMTP data.
  • Initially, I’d get an error when trying to access the smtp section.
  • When published as ClickOnce, the application threw an error and wouldn’t open.

Act I

Let’s create a simple app. First, a Windows Form app with three labels. All the code will be in the same page as the Form1 class, to keep things simple.
image

Add connectionString, appSettings and smtp sections to the app.config.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <connectionStrings>
    <add name="MashDb" 
         connectionString="Server=.\SQLExpress2012;Database=MASH;Trusted Connection=True;" 
         providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
    <add key="Password" value="4077"/>
  </appSettings>
  <system.net>
    <mailSettings>
      <smtp deliveryMethod="Network" from="pierce@mash.net">
        <network
          host="ohmyseoul.kr"
          port="25"
          defaultCredentials="true"/>
      </smtp>
    </mailSettings>
  </system.net>
</configuration>


Add reference to System.Configuration to the project.

image


The static Settings class, to get the app.config data. This is personal preference; I know you can also use the Settings designer for this, but it’s kind of hidden and finicky.
public static class Settings
    {
        public static string MashDb { get { return ConfigurationManager.ConnectionStrings["MashDb"].Name; } }
        public static string Password { get { return ConfigurationManager.AppSettings["Password"]; } }
    }


The static ConfigSettings classes. (Or whatever you’d name your general security utilities.) This is different than what I showed previously. Specifically, they don’t optionally take an executable name. Instead, they take the full path to a config file, essential to solving the ClickOnce problem.
public static class ConfigSettings
    {

        /// <summary>
        /// Encrypts/decrypts the connectionStrings, appSettings and smtp sections.
        /// </summary>
        /// <param name="encryption"></param>
        /// <param name="configPath">Full path to config file</param>
        /// <remarks>https://msdn.microsoft.com/en-us/library/ms254494(v=vs.110).aspx</remarks>
        public static void SetDefaultConfigEncryption(bool encryption, string configPath = null)
        {
            SetConfigSectionEncryption(encryption, "appSettings", configPath);
            SetConfigSectionEncryption(encryption, "connectionStrings", configPath);
            SetConfigSectionEncryption(encryption, "system.net/mailSettings/smtp", configPath);
        }

        /// <summary>
        /// Encrypts/decrypts the connectionStrings, appSettings and smtp sections.
        /// </summary>
        /// <param name="encryption"></param>
        /// <param name="sectionName"></param>
        /// <param name="configPath">Full path to config file</param>
        /// <remarks></remarks>
        public static void SetConfigSectionEncryption(bool encryption, string sectionName, string configPath = null)
        {
            //if no configPath supplied, get the currently executing exe's config.
            if (string.IsNullOrWhiteSpace(configPath))
            {
                configPath = Assembly.GetEntryAssembly().Location + ".config";
#if (DEBUG)
                AppDomain domain = System.AppDomain.CurrentDomain;
                configPath = Path.Combine(domain.SetupInformation.ApplicationBase, domain.FriendlyName + ".config");
#endif
            }

            try
            {
                //Open the configuration file and retrieve the connectionStrings section.
                ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
                fileMap.ExeConfigFilename = configPath;
                Configuration config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
                ConfigurationSection section = config.GetSection(sectionName);
                if (section == null)
                {
                    return; 
                }
                // Encrypt the section.
                if (encryption)
                {
                    if (!section.SectionInformation.IsProtected)
                    {
                        section.SectionInformation.ForceSave = true;
                        section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                    }
                }
                // Remove encryption.
                if (!encryption)
                {
                    if (section.SectionInformation.IsProtected)
                    {
                        section.SectionInformation.UnprotectSection();
                    }
                }
                // Save the current configuration.
                config.Save();
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to encrypt. " + ex.GetBaseException().Message);
            }
        }
    }


In the Form1 class, add the form Load event, and also the method for encrypting the config. You might be tempted to use System.AppDomain.CurrentDomain.FriendlyName, but in a ClickOnce application this will return “DefaultDomain,” and no site I’ve seen has an answer why.

public partial class Form1 : Form
    {
        public Form1()
        {
            EncryptClickOnceConfig();
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            label1.Text = Settings.MashDb;
            label2.Text = Settings.Password;
            label3.Text = new System.Net.Mail.SmtpClient().Host;
        }

        private void EncryptClickOnceConfig()
        {
            //Encrypt config. This didn't work properly in Load. Would lead to error "Unrecognized attribute ‘configProtectionProvider’
            //after encrypting app.config when reading smtp
            //Weird stuff for ClickOnce Deployment. 
            //https://social.msdn.microsoft.com/Forums/windows/en-US/3d7ba97f-684c-46a2-9f7b-5d17e989baa8/encrypting-connection-string-with-clickonce-deployment?forum=winformssetup
            //ClickOnce writes two config files for some reason.
            {
                if (ApplicationDeployment.IsNetworkDeployed && ApplicationDeployment.CurrentDeployment.IsFirstRun)
                {
                    //GetPaths
                    string exeFile = Assembly.GetEntryAssembly().Location;
                    //get directory above the executable's.
                    string parentPath = Path.GetDirectoryName(Path.GetDirectoryName(exeFile));
                    string configName = Path.GetFileName(exeFile) + ".config";
                    //get the matching files
                    foreach (string configFile in Directory.GetDirectories(parentPath, "*" + configName, SearchOption.AllDirectories))
                    {
                        ConfigSettings.SetDefaultConfigEncryption(true, configFile);
                    }
                }
                else
                {
                    //good for debugging, sets the vshost config
                    ConfigSettings.SetDefaultConfigEncryption(true);
                }
            }
        }
    }

Act II

Publish the application. This will automatically create a self-signed certificate. (In a follow up post, I’ll document (for my easy reference) how to create a certificate that lasts more than a year.) There are two important things to do here:
  • Set the project to Release.  This is critical. It should be obvious, but it’s easy to forget and leads to one of the errors.
  • It’s important to install it from a network location, which you can fake by sharing your publish folder and using the UNC path to it. I published to the solution folder. Give the share full permissions (for testing only).

image



You should also enable automatic updating.

image



Install from the UNC path. The app should open.

image



With the app open, open Task Manager, right-click the app, and choose “open file location.”

image



The folder will contain a EncryptedConfigClickOnceSample.exe.config file, which should be encrypted. But you’ll also find another folder, a sibling above or below the one you’re in, also with a EncryptedConfigClickOnceSample.exe.config, which should also be encrypted.

So, fine, it works. How were the problems fixed?

Act III

Encrypt the SMTP Info

This one’s pretty easy. Pass the SetConfigSectionEncryption() method the string “system.net/mailSettings/smtp”. What I found interesting is that you can’t submit just “system.net”, or “system.net/mailSettings” Apparently, in some  Orwellian programming community, some sections are more equal than others.

Error Accessing the SMTP Info

Set the program to Debug mode and republish. Run the app, letting it self-update. You should receive an error
Unrecognized attribute 'configProtectionProvider'

image

Some sites say to call ConfigurationManager.RefreshSection, but the problem really seems to be the behavior when in Debug mode. The error seems to be caused by how configuration info is accessed after the application starts. It’s not clear to me, and no web page mentioned debug mode. But it worked.

ClickOnce Error

In one application, when I tried to install it simply wouldn’t open. I believe this was also due to publishing in Debug mode. However, I also moved my call to configure the config file from Form_Load to the constructor. In my testing app, that’s not making any difference.

Epilogue

Encrypting the app.config could be critical for certain organizations where the highest security is preferred, even if the risk is very low.

References

http://stackoverflow.com/questions/7856951/unrecognized-attribute-configprotectionprovider-after-encrypting-app-config

https://social.msdn.microsoft.com/Forums/windows/en-US/3d7ba97f-684c-46a2-9f7b-5d17e989baa8/encrypting-connection-string-with-clickonce-deployment?forum=winformssetup

Source Code

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Net.Mail;
using System.Configuration;
using System.IO;
using System.Deployment.Application;
using System.Reflection;

namespace EncryptedConfigClickOnceSample
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            EncryptClickOnceConfig();
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            label1.Text = Settings.MashDb;
            label2.Text = Settings.Password;
            label3.Text = new System.Net.Mail.SmtpClient().Host;
        }

        private void EncryptClickOnceConfig()
        {
            //Encrypt config. This didn't work properly in Load. Would lead to error "Unrecognized attribute ‘configProtectionProvider’
            //after encrypting app.config when reading smtp
            //Weird stuff for ClickOnce Deployment. 
            //https://social.msdn.microsoft.com/Forums/windows/en-US/3d7ba97f-684c-46a2-9f7b-5d17e989baa8/encrypting-connection-string-with-clickonce-deployment?forum=winformssetup
            //ClickOnce writes two config files for some reason.
            {
                if (ApplicationDeployment.IsNetworkDeployed && ApplicationDeployment.CurrentDeployment.IsFirstRun)
                {
                    //GetPaths
                    string exeFile = Assembly.GetEntryAssembly().Location;
                    //get directory above the executable's.
                    string parentPath = Path.GetDirectoryName(Path.GetDirectoryName(exeFile));
                    string configName = Path.GetFileName(exeFile) + ".config";
                    //get the matching files
                    foreach (string configFile in Directory.GetFiles(parentPath, "*" + configName, SearchOption.AllDirectories))
                    {
                        ConfigSettings.SetDefaultConfigEncryption(true, configFile);
                    }
                    
                }
                else
                {
#if (DEBUG)
                    //good for debugging, sets the vshost config
                    ConfigSettings.SetDefaultConfigEncryption(true);
#endif
                }
            }
        }
    }

    public static class Settings
    {
        public static string MashDb { get { return ConfigurationManager.ConnectionStrings["MashDb"].Name; } }
        public static string Password { get { return ConfigurationManager.AppSettings["Password"]; } }
    }


    public static class ConfigSettings
    {

        /// <summary>
        /// Encrypts/decrypts the connectionStrings, appSettings and smtp sections.
        /// </summary>
        /// <param name="encryption"></param>
        /// <param name="configPath">Full path to config file</param>
        /// <remarks>https://msdn.microsoft.com/en-us/library/ms254494(v=vs.110).aspx</remarks>
        public static void SetDefaultConfigEncryption(bool encryption, string configPath = null)
        {
            SetConfigSectionEncryption(encryption, "appSettings", configPath);
            SetConfigSectionEncryption(encryption, "connectionStrings", configPath);
            SetConfigSectionEncryption(encryption, "system.net/mailSettings/smtp", configPath);
        }

        /// <summary>
        /// Encrypts/decrypts the connectionStrings, appSettings and smtp sections.
        /// </summary>
        /// <param name="encryption"></param>
        /// <param name="sectionName"></param>
        /// <param name="configPath">Full path to config file</param>
        /// <remarks></remarks>
        public static void SetConfigSectionEncryption(bool encryption, string sectionName, string configPath = null)
        {
            //if no configPath supplied, get the currently executing exe's config.
            if (string.IsNullOrWhiteSpace(configPath))
            {
                configPath = Assembly.GetEntryAssembly().Location + ".config";
#if (DEBUG)
                AppDomain domain = System.AppDomain.CurrentDomain;
                configPath = Path.Combine(domain.SetupInformation.ApplicationBase, domain.FriendlyName + ".config");
#endif
            }

            try
            {
                //Open the configuration file and retrieve the connectionStrings section.
                ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
                fileMap.ExeConfigFilename = configPath;
                Configuration config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
                ConfigurationSection section = config.GetSection(sectionName);
                if (section == null)
                {
                    return; 
                }
                // Encrypt the section.
                if (encryption)
                {
                    if (!section.SectionInformation.IsProtected)
                    {
                        section.SectionInformation.ForceSave = true;
                        section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                    }
                }
                // Remove encryption.
                if (!encryption)
                {
                    if (section.SectionInformation.IsProtected)
                    {
                        section.SectionInformation.UnprotectSection();
                    }
                }
                // Save the current configuration.
                config.Save();
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to encrypt. " + ex.GetBaseException().Message);
            }
        }
    }
}