FiloSottile / age

A simple, modern and secure encryption tool (and Go library) with small explicit keys, no config options, and UNIX-style composability.
https://age-encryption.org
BSD 3-Clause "New" or "Revised" License
15.75k stars 475 forks source link

plugin: add library support for using and providing plugins #485

Open FiloSottile opened 1 year ago

FiloSottile commented 1 year ago

Also discussed in https://github.com/FiloSottile/age/discussions/480.

FiloSottile commented 11 months ago

See the plugin package as exposed in https://pkg.go.dev/filippo.io/age/plugin@v1.1.2-0.20230805193447-c89f0b932ecd.

ngortheone commented 3 months ago

@FiloSottile the link appears to be broken

I am trying to use age programmatically and I have AGE-PLUGIN-... identity

quite commented 3 months ago

@ngortheone the plugin package is available on main by now, since last autumn I believe. But not tagged yet. I'm using it in https://github.com/quite/age-plugin-tkey

AnomalRoil commented 3 months ago

@FiloSottile I have actually started working on plage which would be a "plugin for age" Go library, solving this issue. But I'm not too happy with my current design, especially for the API around the bidirectional Phase 2 parts.

I'd love to discuss designs options further, tho. Phase 1 being unidirectional, it's easy to provide a library with an API for it.

My current design:

  1. Users (plugin creators) can instantiate a plugin, by providing its name and 2 functions as input: a newRec func() RecipientV1 and a newId func() IdentityV1, notice how I currently didn't decide to use the plugin.Recipient type here since it would require the user of the plugin library to specify their own ClientUI and all, but these seemed more of a concern for the age implementation calling the plugin rather than a concern that all plugin creators should be caring about. So RecipientV1 and IdentityV1 are currently interfaces of the form:

    type RecipientV1 interface {
       age.Recipient
       SetRecipient([]byte) error
    }

    (which is arguably not the best kind of Go API, but feels like the easiest way of letting plugin creators define their own types and then passing them to the plage plugin library to handle.)

    The idea of having a plugin object is that it can hold the state, and the details about the plugin.

    This has the advantage of meaning plugin creators mostly just need to create a Wrap and a Unwrap function on their very own recipient and identity types, and a Set([]byte) function without having to care about formatting, stanzas, bech32, etc.

  2. Users can also play with the stdin, stdout and stderr pipes (mostly in order to enable easy testing of plugins) used by a plugin after having instantiated one.

  3. Users can then p.RunPhase1() on their newly instantiated p plugin, which will parse and use the required (state machine) flag for an age-plugin, using the phase 1 state machine to populate its internal state. This is easy because it's being driven by age and is unidirectional. So in theory no need for callbacks or user interactions in this step.

  4. Now comes the tricky bits: Phase 2 is driven by the plugin and is bi-directional. One one side, I could just provide all the required bits like "CommandReader", "CommandWriter" structs, "NewCommand" functions for the various functions supported by the age-plugin spec, and expect the plugin creator to handle the entire thing themselves and basically let the implementation of most of the state machine for Phase 2 to users... But this sounds painful to use.

    On the other hand, if I provide a RunPhase2 function, suddenly all the possible user interactions in that phase become painful and the current "best solution" I could think of would be to provide callbacks to the plugin creators that could be run in certain message types, but this feels brittle at best. But this might be because I'm not tying the callbacks properly to the rights things. Maybe just having a way of tying callbacks to identity and recipients and filekeys would be enough, but it does feel not too comfy to use neither.

    So I'd love to hear your opinion on how best to support plugin creators in Phase 2 without delegating all of the work to them.

    I guess another open question would be key-generation, should I let plugin creators handle that or should the NewPlugin also expect a GenerateIdentity() IdentityV1 function and parse the --generate-key flag or something like that. Notice the current plugin specs just says:

    It is expected that the same plugin binary will be used (potentially with other argument flags) for administrative tasks like generating keys.

so I'm hesitant to provide a "canonical" way of doing key gen, but it does feel like a desirable feature of a plugin library. WDYT?

Finally, re. the current age/plugin package, I think it'd be great if you could also expose the StanzaReader that is in internal/format/format.go since it is required to parse commands and messages that age expects. Do you have a strong reason not to expose it currently?