Closed ezekg closed 6 years ago
Related to #173.
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"
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.
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' }
.
Encryption fields should not be able to be changed after creation.
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.
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.
Max key size should be 200
bytes, since RSA 2048
has a max data size of 245
bytes.
Thought: Maybe use AES for encryptionScheme
v3? Will allow larger datasets to be encrypted.
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:
encryptionScheme = 'AES'
encryptionScheme = 'RSA'
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.
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}"
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?
Or we could just sign keys and skip embedded metadata… or add a separate signed
attribute?
(We should do this later on.)
Potential customer, Serveo, who seems to want the signed keys (good validation to keep it).
Example setup for showcasing signed keys:
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')
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))
)
For above: https://github.com/bushev/nodejs-license-file. ☝️
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.