keygen-sh / keygen-api

Keygen is a fair source software licensing and distribution API built with Ruby on Rails. For developers, by developers.
https://keygen.sh
Other
836 stars 56 forks source link

Encrypted keys should provide a public key to decrypt encoded metadata #178

Closed ezekg closed 6 years ago

ezekg commented 6 years ago

Encrypted keys should offer the ability to encode customer data within the license key, e.g. user email, name, etc. and should offer a public key which can be used to decode the data client-side. A private key would need to be generated alongside every encrypted policy, and encoded fields should be selectable by the account admin.

This would make encrypted keys more useful for e.g. offline use, where the public key can be stored client-side for offline license key validation.

ezekg commented 6 years ago

Related to #173.

ezekg commented 6 years ago

Convo with the founder of Timing,

Zeke: I've been throwing around an idea of generating encrypted licenses that can be populated with metadata (so similar to your protobufs, containing email, name, etc.), and then each account gets a pub key generated that they can use to decrypt the license offline. Additional validation concerning machines has to be done over the wire, but you get a good base-case for offline use.

With an SDK and that—would something like that be valuable to you?

Daniel: Yes, that sounds like what I'd want. I was just about to say "you're mostly there — slap a signature onto your licenses and give me an SDK to request and consume them in a convenient, somewhat tamper-proof manner (that's where the source SDK comes in), and you have it"

ezekg commented 6 years ago

I've removed mentions of the old encrypted licensing scheme from the docs. The old scheme is being used, so we'll need to add logic to account for older policies using the v1 scheme and continue to generate licenses with that scheme and not the new one. We can add an attribute to the policy like encryptionScheme: 'v1' or lock the scheme based on the policy's creation date.

ezekg commented 6 years ago

Add an attribute like encryptionFields: ['user.id', 'user.email', 'user.metadata.customerId'] to specify which fields get encoded into the license key, and in what order.

Or maybe just a scalar hash like metadata e.g. encryptionData: { userId: 1, userEmail: 'foo@example.com', customerId: 'cus_0123456789' }.

ezekg commented 6 years ago

Encryption fields should not be able to be changed after creation.

ezekg commented 6 years ago

Having second thoughts on this, as it introduces a lot of complexity. May just write an example implementation in Swift using Keygen and protobufs for offline license support.

ezekg commented 6 years ago

I think the easiest way to implement this would be to allow an arbitrary string to be passed as a key, like we're doing now, and then encrypt the key with the account's private key. The encrypted key can then be decoded using the account's public key to retrieve any encoded data, e.g. JSON, etc.

We need to prioritize, as this would allow offline licensing to be easier to implement.

ezekg commented 6 years ago

Max key size should be 200 bytes, since RSA 2048 has a max data size of 245 bytes.

ezekg commented 6 years ago

Thought: Maybe use AES for encryptionScheme v3? Will allow larger datasets to be encrypted.

ezekg commented 6 years ago

To support large license key data, we need to use AES. I think we should allow multiple encryption schemes, so the customer can opt-in to whichever fits their use case the best:

And then encryptionScheme = null means it's using v1 (hashing). We will need to update the account's publicKey meta to be rsaPublicKey, and then add aesKey and aesIv.

It should not be able to be changed.

ezekg commented 6 years ago

Using AES:

require 'openssl'
require 'base64'

data = "Very, very confidential data\n" * 100

cipher = OpenSSL::Cipher::AES256.new :CBC
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv

encrypted = cipher.update(data) + cipher.final

decipher = OpenSSL::Cipher::AES256.new :CBC
decipher.decrypt
decipher.key = key
decipher.iv = iv

plain = decipher.update(encrypted) + decipher.final

puts "key = #{Base64.strict_encode64(key)}"
puts "iv = #{Base64.strict_encode64(iv)}"
puts "match = #{data == plain}"
ezekg commented 6 years ago

I don’t like the idea of using AES, actually. It opens up the possibility of keygens being created, and rather easily at that. Maybe we need to use 4096-bit RSA keys?

ezekg commented 6 years ago

Or we could just sign keys and skip embedded metadata… or add a separate signed attribute?

(We should do this later on.)

ezekg commented 6 years ago

Potential customer, Serveo, who seems to want the signed keys (good validation to keep it).

ezekg commented 6 years ago

Example setup for showcasing signed keys:

Server

const crypto = require('crypto')

// License payload
const payload = {
  key: crypto.randomBytes(16).toString('hex'),
  customer: 'foobar@example.com',
  allowances: 3
}

// Convert JSON key payload into a buffer and then encode into base64
const key = Buffer.from(JSON.stringify(payload)).toString('base64')

// TODO(ezekg) Store license key in Keygen using the RSA_2048_SIGN encryption
//             scheme, then encode into a license file along with the key's
//             generated RSA signature for verification purposes.
const sig = ''

// Combine key and signature into a "license file"
const licenseFile = `${key}:${sig}`

// TODO(ezekg) Deliver license file to customer
const hex = Buffer.from(licenseFile).toString('hex')

Client

const { KEYGEN_PUBLIC_KEY } = process.env
const crypto = require('crypto')
const chalk = require('chalk')

// Decode the license file's contents
const [key, sig] = licenseFile.split(':')

// Verify the license key's contents
try {
  const verifier = crypto.createVerify('sha256')
  verifier.write(key)
  verifier.end()

  const v = verifier.verify(KEYGEN_PUBLIC_KEY, sig, 'base64')
  if (!v) {
    throw new Error('License key failed signature verification')
  }
} catch (e) {
  console.error(
    chalk.red(e.message)
  )

  process.exit(1)
}

// Decode JSON key payload
const buf = Buffer.from(key, 'base64')
const obj = JSON.parse(buf.toString())

console.log(
  chalk.green(JSON.stringify(obj, null, 2))
)
ezekg commented 6 years ago

For above: https://github.com/bushev/nodejs-license-file. ☝️