age.rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ext/Makefile +22 -0
- data/ext/age.go +232 -0
- data/ext/extconf.rb +29 -0
- data/ext/go.mod +12 -0
- data/ext/go.sum +14 -0
- data/lib/age/bindings.rb +43 -0
- data/lib/age/errors.rb +42 -0
- data/lib/age/version.rb +5 -0
- data/lib/age.rb +370 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6135f621a6b58de2582dea23188562bc6c5203c5218ebf4ac52fa034c743680f
|
|
4
|
+
data.tar.gz: 0a0fb8788f8aee9f39fa25dc74f9fb907ae2e4e1d213bdd47b28b0cba4b44c30
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4458de0a38dcdb52bba5ee03a31ba873f867fa731df40c94d780f510a712f217c8d5a413f7cb78796fb24e1d6873d325164fb5756a716d4dc01004f127f8302c
|
|
7
|
+
data.tar.gz: d4435de3d279c8bb24c38635237e3627e1b6eb7b4c9f471e0ce3e37cd9d50d5ab45a9af8f29fecb674deeab0c33b1d736db86195c3cdafcc8d314f6dcaa316df
|
data/ext/Makefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.PHONY: all
|
|
2
|
+
all: vet fmt lint build
|
|
3
|
+
|
|
4
|
+
.PHONY: vet
|
|
5
|
+
vet:
|
|
6
|
+
go vet ./...
|
|
7
|
+
|
|
8
|
+
.PHONY: fmt
|
|
9
|
+
fmt:
|
|
10
|
+
go fmt ./...
|
|
11
|
+
|
|
12
|
+
.PHONY: lint
|
|
13
|
+
lint:
|
|
14
|
+
golangci-lint run ./...
|
|
15
|
+
|
|
16
|
+
.PHONY: build
|
|
17
|
+
build:
|
|
18
|
+
go build -o ../age.so -buildmode=c-shared age.go
|
|
19
|
+
|
|
20
|
+
.PHONY: clean
|
|
21
|
+
clean:
|
|
22
|
+
rm -f ../age.so ../age.h
|
data/ext/age.go
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
|
|
6
|
+
typedef struct {
|
|
7
|
+
char* data;
|
|
8
|
+
int length;
|
|
9
|
+
} AgeInput;
|
|
10
|
+
|
|
11
|
+
typedef struct {
|
|
12
|
+
char** data;
|
|
13
|
+
int* length;
|
|
14
|
+
} AgeOutput;
|
|
15
|
+
|
|
16
|
+
typedef struct {
|
|
17
|
+
char** pubkey;
|
|
18
|
+
char** privkey;
|
|
19
|
+
} AgeKeyPair;
|
|
20
|
+
*/
|
|
21
|
+
import "C"
|
|
22
|
+
|
|
23
|
+
import (
|
|
24
|
+
"bytes"
|
|
25
|
+
"io"
|
|
26
|
+
"strings"
|
|
27
|
+
"unsafe"
|
|
28
|
+
|
|
29
|
+
"filippo.io/age"
|
|
30
|
+
"filippo.io/age/agessh"
|
|
31
|
+
"filippo.io/age/armor"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
func __performEncryption(input *C.AgeInput, output *C.AgeOutput, armored C.int, recipients ...age.Recipient) *C.char {
|
|
35
|
+
var buffer bytes.Buffer
|
|
36
|
+
var writer io.Writer = &buffer
|
|
37
|
+
var armorWriter io.WriteCloser
|
|
38
|
+
|
|
39
|
+
if armored != 0 {
|
|
40
|
+
armorWriter = armor.NewWriter(writer)
|
|
41
|
+
writer = armorWriter
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
encryptor, err := age.Encrypt(writer, recipients...)
|
|
45
|
+
if err != nil {
|
|
46
|
+
return C.CString("Error during encryption setup: " + err.Error())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
plainBytes := C.GoBytes(unsafe.Pointer(input.data), C.int(input.length))
|
|
50
|
+
if _, err := encryptor.Write(plainBytes); err != nil {
|
|
51
|
+
return C.CString("Error during encryption: " + err.Error())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if err := encryptor.Close(); err != nil {
|
|
55
|
+
return C.CString("Error closing encryption stream: " + err.Error())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if armorWriter != nil {
|
|
59
|
+
if err := armorWriter.Close(); err != nil {
|
|
60
|
+
return C.CString("Error closing armor writer: " + err.Error())
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
bytes := buffer.Bytes()
|
|
65
|
+
*output.length = C.int(len(bytes))
|
|
66
|
+
*output.data = (*C.char)(C.CBytes(bytes))
|
|
67
|
+
|
|
68
|
+
return nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func __performDecryption(input *C.AgeInput, output *C.AgeOutput, armored C.int, identities ...age.Identity) *C.char {
|
|
72
|
+
encryptedBytes := C.GoBytes(unsafe.Pointer(input.data), C.int(input.length))
|
|
73
|
+
var reader io.Reader = bytes.NewReader(encryptedBytes)
|
|
74
|
+
|
|
75
|
+
if armored != 0 {
|
|
76
|
+
reader = armor.NewReader(reader)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
decryptor, err := age.Decrypt(reader, identities...)
|
|
80
|
+
if err != nil {
|
|
81
|
+
return C.CString("Error during decryption setup: " + err.Error())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var buffer bytes.Buffer
|
|
85
|
+
if _, err = buffer.ReadFrom(decryptor); err != nil {
|
|
86
|
+
return C.CString("Error during decryption: " + err.Error())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
plainBytes := buffer.Bytes()
|
|
90
|
+
*output.length = C.int(len(plainBytes))
|
|
91
|
+
*output.data = (*C.char)(C.CBytes(plainBytes))
|
|
92
|
+
|
|
93
|
+
return nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//export encrypt
|
|
97
|
+
func encrypt(pubKeyStrs *C.char, input *C.AgeInput, output *C.AgeOutput, armored C.int) *C.char {
|
|
98
|
+
pubKeyStrList := strings.Split(C.GoString(pubKeyStrs), ",")
|
|
99
|
+
recipients := []age.Recipient{}
|
|
100
|
+
|
|
101
|
+
for _, pubKeyStr := range pubKeyStrList {
|
|
102
|
+
var pubKey age.Recipient
|
|
103
|
+
var err error
|
|
104
|
+
|
|
105
|
+
if strings.HasPrefix(pubKeyStr, "age1pq1") {
|
|
106
|
+
pubKey, err = age.ParseHybridRecipient(pubKeyStr)
|
|
107
|
+
if err != nil {
|
|
108
|
+
return C.CString("Error parsing hybrid public key: " + err.Error())
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
pubKey, err = age.ParseX25519Recipient(pubKeyStr)
|
|
112
|
+
if err != nil {
|
|
113
|
+
return C.CString("Error parsing public key: " + err.Error())
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
recipients = append(recipients, pubKey)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return __performEncryption(input, output, armored, recipients...)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//export decrypt
|
|
123
|
+
func decrypt(privKeyStrs *C.char, input *C.AgeInput, output *C.AgeOutput, armored C.int) *C.char {
|
|
124
|
+
privKeyStrList := strings.Split(C.GoString(privKeyStrs), ",")
|
|
125
|
+
identities := []age.Identity{}
|
|
126
|
+
|
|
127
|
+
for _, privKeyStr := range privKeyStrList {
|
|
128
|
+
var privKey age.Identity
|
|
129
|
+
var err error
|
|
130
|
+
|
|
131
|
+
if strings.HasPrefix(privKeyStr, "AGE-SECRET-KEY-PQ") {
|
|
132
|
+
privKey, err = age.ParseHybridIdentity(privKeyStr)
|
|
133
|
+
if err != nil {
|
|
134
|
+
return C.CString("Error parsing hybrid private key: " + err.Error())
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
privKey, err = age.ParseX25519Identity(privKeyStr)
|
|
138
|
+
if err != nil {
|
|
139
|
+
return C.CString("Error parsing private key: " + err.Error())
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
identities = append(identities, privKey)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return __performDecryption(input, output, armored, identities...)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//export generate_keypair
|
|
149
|
+
func generate_keypair(keypair *C.AgeKeyPair, pq C.int) *C.char {
|
|
150
|
+
var pubKey string
|
|
151
|
+
var privKey string
|
|
152
|
+
|
|
153
|
+
if pq != 0 {
|
|
154
|
+
identity, err := age.GenerateHybridIdentity()
|
|
155
|
+
if err != nil {
|
|
156
|
+
return C.CString("Error generating hybrid keypair: " + err.Error())
|
|
157
|
+
}
|
|
158
|
+
pubKey = identity.Recipient().String()
|
|
159
|
+
privKey = identity.String()
|
|
160
|
+
} else {
|
|
161
|
+
identity, err := age.GenerateX25519Identity()
|
|
162
|
+
if err != nil {
|
|
163
|
+
return C.CString("Error generating keypair: " + err.Error())
|
|
164
|
+
}
|
|
165
|
+
pubKey = identity.Recipient().String()
|
|
166
|
+
privKey = identity.String()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
*keypair.pubkey = C.CString(pubKey)
|
|
170
|
+
*keypair.privkey = C.CString(privKey)
|
|
171
|
+
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//export encrypt_with_passphrase
|
|
176
|
+
func encrypt_with_passphrase(passphrase *C.char, input *C.AgeInput, output *C.AgeOutput, armored C.int) *C.char {
|
|
177
|
+
recipient, err := age.NewScryptRecipient(C.GoString(passphrase))
|
|
178
|
+
if err != nil {
|
|
179
|
+
return C.CString("Error creating scrypt recipient: " + err.Error())
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return __performEncryption(input, output, armored, recipient)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//export decrypt_with_passphrase
|
|
186
|
+
func decrypt_with_passphrase(passphrase *C.char, input *C.AgeInput, output *C.AgeOutput, armored C.int) *C.char {
|
|
187
|
+
identity, err := age.NewScryptIdentity(C.GoString(passphrase))
|
|
188
|
+
if err != nil {
|
|
189
|
+
return C.CString("Error creating scrypt identity: " + err.Error())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return __performDecryption(input, output, armored, identity)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//export encrypt_with_ssh_keys
|
|
196
|
+
func encrypt_with_ssh_keys(sshPubKeyStrs *C.char, input *C.AgeInput, output *C.AgeOutput, armored C.int) *C.char {
|
|
197
|
+
sshPubKeyStrList := strings.Split(C.GoString(sshPubKeyStrs), ",")
|
|
198
|
+
recipients := []age.Recipient{}
|
|
199
|
+
|
|
200
|
+
for _, sshPubKeyStr := range sshPubKeyStrList {
|
|
201
|
+
recipient, err := agessh.ParseRecipient(sshPubKeyStr)
|
|
202
|
+
if err != nil {
|
|
203
|
+
return C.CString("Error parsing SSH public key: " + err.Error())
|
|
204
|
+
}
|
|
205
|
+
recipients = append(recipients, recipient)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return __performEncryption(input, output, armored, recipients...)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//export decrypt_with_ssh_keys
|
|
212
|
+
func decrypt_with_ssh_keys(sshPrivKeyStrs *C.char, input *C.AgeInput, output *C.AgeOutput, armored C.int) *C.char {
|
|
213
|
+
sshPrivKeyStrList := strings.Split(C.GoString(sshPrivKeyStrs), ",")
|
|
214
|
+
identities := []age.Identity{}
|
|
215
|
+
|
|
216
|
+
for _, sshPrivKeyStr := range sshPrivKeyStrList {
|
|
217
|
+
identity, err := agessh.ParseIdentity([]byte(sshPrivKeyStr))
|
|
218
|
+
if err != nil {
|
|
219
|
+
return C.CString("Error parsing SSH private key: " + err.Error())
|
|
220
|
+
}
|
|
221
|
+
identities = append(identities, identity)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return __performDecryption(input, output, armored, identities...)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
//export free_memory
|
|
228
|
+
func free_memory(ptr *C.char) {
|
|
229
|
+
C.free(unsafe.Pointer(ptr))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func main() {}
|
data/ext/extconf.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mkmf'
|
|
4
|
+
|
|
5
|
+
# Check if Go is installed
|
|
6
|
+
go_bin = find_executable('go')
|
|
7
|
+
abort 'Go is required to build this gem. Please install Go from https://golang.org/dl/' unless go_bin
|
|
8
|
+
|
|
9
|
+
# Build the CGO shared library
|
|
10
|
+
puts 'Building CGO shared library...'
|
|
11
|
+
Dir.chdir(__dir__) do
|
|
12
|
+
abort 'age.go not found in ext directory. The gem source files may be corrupted.' unless File.exist?('age.go')
|
|
13
|
+
|
|
14
|
+
output_path = File.join('..', 'age.so')
|
|
15
|
+
unless system(go_bin, 'build', '-o', output_path, '-buildmode=c-shared', 'age.go')
|
|
16
|
+
abort 'Failed to build CGO shared library. Ensure Go is properly installed and dependencies are available.'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create a dummy Makefile since we've already built the library
|
|
21
|
+
File.write('Makefile', <<~MAKEFILE)
|
|
22
|
+
.PHONY: install clean
|
|
23
|
+
|
|
24
|
+
install:
|
|
25
|
+
\t@echo "Shared library already built"
|
|
26
|
+
|
|
27
|
+
clean:
|
|
28
|
+
\t@echo "Nothing to clean"
|
|
29
|
+
MAKEFILE
|
data/ext/go.mod
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module github.com/tschaefer/ruby-age
|
|
2
|
+
|
|
3
|
+
go 1.25.3
|
|
4
|
+
|
|
5
|
+
require filippo.io/age v1.3.1
|
|
6
|
+
|
|
7
|
+
require (
|
|
8
|
+
filippo.io/edwards25519 v1.1.0 // indirect
|
|
9
|
+
filippo.io/hpke v0.4.0 // indirect
|
|
10
|
+
golang.org/x/crypto v0.45.0 // indirect
|
|
11
|
+
golang.org/x/sys v0.38.0 // indirect
|
|
12
|
+
)
|
data/ext/go.sum
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
|
2
|
+
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
|
3
|
+
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
|
|
4
|
+
filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4=
|
|
5
|
+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|
6
|
+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
7
|
+
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
|
8
|
+
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
|
9
|
+
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
|
10
|
+
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
|
11
|
+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
12
|
+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
13
|
+
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
|
14
|
+
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
data/lib/age/bindings.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
module Age
|
|
6
|
+
##
|
|
7
|
+
# FFI bindings for the age cgo library.
|
|
8
|
+
module Bindings
|
|
9
|
+
extend FFI::Library
|
|
10
|
+
|
|
11
|
+
ffi_lib File.expand_path('../../age.so', __dir__)
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# Struct for input data to encryption/decryption functions.
|
|
15
|
+
class AgeInput < FFI::Struct
|
|
16
|
+
layout :data, :pointer,
|
|
17
|
+
:length, :int
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Struct for output data from encryption/decryption functions.
|
|
22
|
+
class AgeOutput < FFI::Struct
|
|
23
|
+
layout :data, :pointer,
|
|
24
|
+
:length, :pointer
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Struct for age key pairs.
|
|
29
|
+
class AgeKeyPair < FFI::Struct
|
|
30
|
+
layout :public_key, :pointer,
|
|
31
|
+
:private_key, :pointer
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attach_function :encrypt, [:string, AgeInput.by_ref, AgeOutput.by_ref, :int], :pointer
|
|
35
|
+
attach_function :decrypt, [:string, AgeInput.by_ref, AgeOutput.by_ref, :int], :pointer
|
|
36
|
+
attach_function :encrypt_with_passphrase, [:string, AgeInput.by_ref, AgeOutput.by_ref, :int], :pointer
|
|
37
|
+
attach_function :decrypt_with_passphrase, [:string, AgeInput.by_ref, AgeOutput.by_ref, :int], :pointer
|
|
38
|
+
attach_function :encrypt_with_ssh_keys, [:string, AgeInput.by_ref, AgeOutput.by_ref, :int], :pointer
|
|
39
|
+
attach_function :decrypt_with_ssh_keys, [:string, AgeInput.by_ref, AgeOutput.by_ref, :int], :pointer
|
|
40
|
+
attach_function :generate_keypair, [AgeKeyPair.by_ref, :int], :pointer
|
|
41
|
+
attach_function :free_memory, [:pointer], :void
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/age/errors.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Age
|
|
4
|
+
##
|
|
5
|
+
# Custom error class for encryption errors.
|
|
6
|
+
class EncryptionError < StandardError
|
|
7
|
+
##
|
|
8
|
+
# @return [String, nil] reason for the encryption error
|
|
9
|
+
attr_reader :reason
|
|
10
|
+
|
|
11
|
+
def initialize(reason = nil)
|
|
12
|
+
@reason = reason
|
|
13
|
+
super('Encryption failed.')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Custom error class for decryption errors.
|
|
19
|
+
class DecryptionError < StandardError
|
|
20
|
+
##
|
|
21
|
+
# @return [String, nil] reason for the decryption error
|
|
22
|
+
attr_reader :reason
|
|
23
|
+
|
|
24
|
+
def initialize(reason = nil)
|
|
25
|
+
@reason = reason
|
|
26
|
+
super('Decryption failed.')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Custom error class for generate key pair errors.
|
|
32
|
+
class GenerateKeyPairError < StandardError
|
|
33
|
+
##
|
|
34
|
+
# @return [String, nil] reason for the key pair generation error
|
|
35
|
+
attr_reader :reason
|
|
36
|
+
|
|
37
|
+
def initialize(reason = nil)
|
|
38
|
+
@reason = reason
|
|
39
|
+
super('Generation key pair failed.')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/age/version.rb
ADDED
data/lib/age.rb
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
require 'age/bindings'
|
|
6
|
+
require 'age/errors'
|
|
7
|
+
require 'age/version'
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# Ruby bindings for [age](https://github.com/FiloSottile/age) using a CGO shared
|
|
11
|
+
# library with FFI bindings.
|
|
12
|
+
#
|
|
13
|
+
# Age is a simple, modern, and secure file encryption tool, format, and Go
|
|
14
|
+
# library. This gem provides a Ruby interface to age's encryption and decryption
|
|
15
|
+
# capabilities.
|
|
16
|
+
#
|
|
17
|
+
# Features:
|
|
18
|
+
#
|
|
19
|
+
# - Encrypt and decrypt data using age public/private key pairs
|
|
20
|
+
# - Encrypt and decrypt files directly
|
|
21
|
+
# - Generate age keypairs programmatically
|
|
22
|
+
# - Multiple recipients support for encryption
|
|
23
|
+
# - ASCII armor format support for text-safe encrypted output
|
|
24
|
+
# - FFI-based integration with Go's age implementation
|
|
25
|
+
# - Binary data handling with proper encoding
|
|
26
|
+
module Age
|
|
27
|
+
class << self
|
|
28
|
+
##
|
|
29
|
+
# Encrypts plain data using the provided age public keys.
|
|
30
|
+
#
|
|
31
|
+
# @param pubkeys [Array<String>] List of age public keys.
|
|
32
|
+
# @param plain [Bytes] Plain data to encrypt.
|
|
33
|
+
# @param armor [Boolean] Whether to armor the output.
|
|
34
|
+
#
|
|
35
|
+
# @return [Bytes] Encrypted data.
|
|
36
|
+
def encrypt(pubkeys, plain, armor: false)
|
|
37
|
+
pubkeys = pubkeys.join(',') if pubkeys.is_a?(Array)
|
|
38
|
+
perform_encryption(plain) do |input, output|
|
|
39
|
+
Age::Bindings.encrypt(pubkeys, input, output, armor ? 1 : 0)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
## Encrypts plain data using the provided passphrase.
|
|
44
|
+
#
|
|
45
|
+
# @param passphrase [String] Passphrase to use for encryption.
|
|
46
|
+
# @param plain [Bytes] Plain data to encrypt.
|
|
47
|
+
# @param armor [Boolean] Whether to armor the output.
|
|
48
|
+
#
|
|
49
|
+
# @return [Bytes] Encrypted data.
|
|
50
|
+
def encrypt_with_passphrase(passphrase, plain, armor: false)
|
|
51
|
+
perform_encryption(plain) do |input, output|
|
|
52
|
+
Age::Bindings.encrypt_with_passphrase(passphrase, input, output, armor ? 1 : 0)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Encrypts plain data using the provided SSH public keys.
|
|
58
|
+
#
|
|
59
|
+
# @param ssh_pubkeys [Array<String>] List of SSH public keys (ssh-rsa, ssh-ed25519).
|
|
60
|
+
# @param plain [Bytes] Plain data to encrypt.
|
|
61
|
+
# @param armor [Boolean] Whether to armor the output.
|
|
62
|
+
#
|
|
63
|
+
# @return [Bytes] Encrypted data.
|
|
64
|
+
def encrypt_with_ssh_keys(ssh_pubkeys, plain, armor: false)
|
|
65
|
+
ssh_pubkeys = ssh_pubkeys.join(',') if ssh_pubkeys.is_a?(Array)
|
|
66
|
+
perform_encryption(plain) do |input, output|
|
|
67
|
+
Age::Bindings.encrypt_with_ssh_keys(ssh_pubkeys, input, output, armor ? 1 : 0)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Decrypts encrypted data using the provided age private keys.
|
|
73
|
+
#
|
|
74
|
+
# @param privkeys [Array<String>] List of age private keys.
|
|
75
|
+
# @param encrypted [Bytes] Encrypted data to decrypt.
|
|
76
|
+
# @param armor [Boolean] Whether the input is armored.
|
|
77
|
+
#
|
|
78
|
+
# @return [Bytes] Decrypted plain data.
|
|
79
|
+
def decrypt(privkeys, encrypted, armor: false)
|
|
80
|
+
privkeys = privkeys.join(',') if privkeys.is_a?(Array)
|
|
81
|
+
perform_decryption(encrypted) do |input, output|
|
|
82
|
+
Age::Bindings.decrypt(privkeys, input, output, armor ? 1 : 0)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Decrypts encrypted data using the provided passphrase.
|
|
88
|
+
#
|
|
89
|
+
# @param passphrase [String] Passphrase to use for decryption.
|
|
90
|
+
# @param encrypted [Bytes] Encrypted data to decrypt.
|
|
91
|
+
# @param armor [Boolean] Whether the input is armored.
|
|
92
|
+
#
|
|
93
|
+
# @return [Bytes] Decrypted plain data.
|
|
94
|
+
def decrypt_with_passphrase(passphrase, encrypted, armor: false)
|
|
95
|
+
perform_decryption(encrypted) do |input, output|
|
|
96
|
+
Age::Bindings.decrypt_with_passphrase(passphrase, input, output, armor ? 1 : 0)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# Decrypts encrypted data using the provided SSH private keys.
|
|
102
|
+
#
|
|
103
|
+
# @param ssh_privkeys [Array<String>] List of SSH private keys (ssh-rsa, ssh-ed25519).
|
|
104
|
+
# @param encrypted [Bytes] Encrypted data to decrypt.
|
|
105
|
+
# @param armor [Boolean] Whether the input is armored.
|
|
106
|
+
#
|
|
107
|
+
# @return [Bytes] Decrypted plain data.
|
|
108
|
+
def decrypt_with_ssh_keys(ssh_privkeys, encrypted, armor: false)
|
|
109
|
+
ssh_privkeys = ssh_privkeys.join(',') if ssh_privkeys.is_a?(Array)
|
|
110
|
+
perform_decryption(encrypted) do |input, output|
|
|
111
|
+
Age::Bindings.decrypt_with_ssh_keys(ssh_privkeys, input, output, armor ? 1 : 0)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
## Encrypts a file using the provided age public keys.
|
|
116
|
+
#
|
|
117
|
+
# @param pubkeys [Array<String>] List of age public keys.
|
|
118
|
+
# @param infile [String] Input file path.
|
|
119
|
+
# @param outfile [String, nil] Output file path. If nil, appends `.age` to infile.
|
|
120
|
+
# @param armor [Boolean] Whether to armor the output.
|
|
121
|
+
#
|
|
122
|
+
# @return [void]
|
|
123
|
+
def encrypt_file(pubkeys, infile, outfile = nil, armor: false)
|
|
124
|
+
perform_file_encryption(infile, outfile) do |plain|
|
|
125
|
+
encrypt(pubkeys, plain, armor:)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Encrypts a file using the provided passphrase.
|
|
131
|
+
#
|
|
132
|
+
# @param passphrase [String] Passphrase to use for encryption.
|
|
133
|
+
# @param infile [String] Input file path.
|
|
134
|
+
# @param outfile [String, nil] Output file path. If nil, appends `.age` to infile.
|
|
135
|
+
# @param armor [Boolean] Whether to armor the output.
|
|
136
|
+
#
|
|
137
|
+
# @return [void]
|
|
138
|
+
def encrypt_file_with_passphrase(passphrase, infile, outfile = nil, armor: false)
|
|
139
|
+
perform_file_encryption(infile, outfile) do |plain|
|
|
140
|
+
encrypt_with_passphrase(passphrase, plain, armor:)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Encrypts a file using the provided SSH public keys.
|
|
146
|
+
#
|
|
147
|
+
# @param ssh_pubkeys [Array<String>] List of SSH public keys (ssh-rsa, ssh-ed25519).
|
|
148
|
+
# @param infile [String] Input file path.
|
|
149
|
+
# @param outfile [String, nil] Output file path. If nil, appends `.age` to infile.
|
|
150
|
+
# @param armor [Boolean] Whether to armor the output.
|
|
151
|
+
#
|
|
152
|
+
# @return [void]
|
|
153
|
+
def encrypt_file_with_ssh_keys(ssh_pubkeys, infile, outfile = nil, armor: false)
|
|
154
|
+
perform_file_encryption(infile, outfile) do |plain|
|
|
155
|
+
encrypt_with_ssh_keys(ssh_pubkeys, plain, armor:)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
## Decrypts a file using the provided age private keys.
|
|
160
|
+
#
|
|
161
|
+
# @param privkeys [Array<String>] List of age private keys.
|
|
162
|
+
# @param infile [String] Input file path.
|
|
163
|
+
# @param outfile [String, nil] Output file path. If nil, removes `.age` from infile.
|
|
164
|
+
#
|
|
165
|
+
# @return [void]
|
|
166
|
+
def decrypt_file(privkeys, infile, outfile = nil)
|
|
167
|
+
perform_file_decryption(infile, outfile) do |encrypted, armor|
|
|
168
|
+
decrypt(privkeys, encrypted, armor:)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
##
|
|
173
|
+
# Decrypts a file using the provided passphrase.
|
|
174
|
+
#
|
|
175
|
+
# @param passphrase [String] Passphrase to use for decryption.
|
|
176
|
+
# @param infile [String] Input file path.
|
|
177
|
+
# @param outfile [String, nil] Output file path. If nil, removes `.age` from infile.
|
|
178
|
+
#
|
|
179
|
+
# @return [void]
|
|
180
|
+
def decrypt_file_with_passphrase(passphrase, infile, outfile = nil)
|
|
181
|
+
perform_file_decryption(infile, outfile) do |encrypted, armor|
|
|
182
|
+
decrypt_with_passphrase(passphrase, encrypted, armor:)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
##
|
|
187
|
+
# Decrypts a file using the provided SSH private keys.
|
|
188
|
+
#
|
|
189
|
+
# @param ssh_privkeys [Array<String>] List of SSH private keys (ssh-rsa, ssh-ed25519).
|
|
190
|
+
# @param infile [String] Input file path.
|
|
191
|
+
# @param outfile [String, nil] Output file path. If nil, removes `.age` from infile.
|
|
192
|
+
#
|
|
193
|
+
# @return [void]
|
|
194
|
+
def decrypt_file_with_ssh_keys(ssh_privkeys, infile, outfile = nil)
|
|
195
|
+
perform_file_decryption(infile, outfile) do |encrypted, armor|
|
|
196
|
+
decrypt_with_ssh_keys(ssh_privkeys, encrypted, armor:)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
##
|
|
201
|
+
# Generates a new age key pair.
|
|
202
|
+
#
|
|
203
|
+
# @param postquantum [Boolean] Whether to generate a post-quantum key pair.
|
|
204
|
+
#
|
|
205
|
+
# @return [Hash{Symbol => String}] A hash containing :public_key and :private_key.
|
|
206
|
+
def generate_keypair(postquantum: false)
|
|
207
|
+
pubkey_ptr = FFI::MemoryPointer.new(:pointer)
|
|
208
|
+
privkey_ptr = FFI::MemoryPointer.new(:pointer)
|
|
209
|
+
keypair = Age::Bindings::AgeKeyPair.new
|
|
210
|
+
keypair[:public_key] = pubkey_ptr
|
|
211
|
+
keypair[:private_key] = privkey_ptr
|
|
212
|
+
|
|
213
|
+
err_ptr = Age::Bindings.generate_keypair(keypair, postquantum ? 1 : 0)
|
|
214
|
+
unless err_ptr.null?
|
|
215
|
+
err_msg = read_string_from_pointer(err_ptr)
|
|
216
|
+
raise GenerateKeyPairError, err_msg
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
pubkey = read_string_from_pointer(pubkey_ptr.read_pointer)
|
|
220
|
+
privkey = read_string_from_pointer(privkey_ptr.read_pointer)
|
|
221
|
+
|
|
222
|
+
{ public_key: pubkey, private_key: privkey }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
##
|
|
226
|
+
# Performs encryption operation with common FFI setup.
|
|
227
|
+
#
|
|
228
|
+
# @param plain [Bytes] Plain data to encrypt.
|
|
229
|
+
# @yield [input, output] Block that performs the actual encryption call.
|
|
230
|
+
#
|
|
231
|
+
# @return [Bytes] Encrypted data.
|
|
232
|
+
def perform_encryption(plain)
|
|
233
|
+
plain = plain.b if plain.respond_to?(:b)
|
|
234
|
+
|
|
235
|
+
plain_ptr = FFI::MemoryPointer.new(:char, plain.bytesize)
|
|
236
|
+
plain_ptr.put_bytes(0, plain)
|
|
237
|
+
|
|
238
|
+
input = Age::Bindings::AgeInput.new
|
|
239
|
+
input[:data] = plain_ptr
|
|
240
|
+
input[:length] = plain.bytesize
|
|
241
|
+
|
|
242
|
+
encrypted_ptr = FFI::MemoryPointer.new(:pointer)
|
|
243
|
+
encrypted_len_ptr = FFI::MemoryPointer.new(:int)
|
|
244
|
+
|
|
245
|
+
output = Age::Bindings::AgeOutput.new
|
|
246
|
+
output[:data] = encrypted_ptr
|
|
247
|
+
output[:length] = encrypted_len_ptr
|
|
248
|
+
|
|
249
|
+
err_ptr = yield(input, output)
|
|
250
|
+
unless err_ptr.null?
|
|
251
|
+
err_msg = read_string_from_pointer(err_ptr)
|
|
252
|
+
raise EncryptionError, err_msg
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
bytes = read_bytes_from_pointer(encrypted_ptr.read_pointer, encrypted_len_ptr.read_int)
|
|
256
|
+
bytes.force_encoding('BINARY')
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
##
|
|
260
|
+
# Performs decryption operation with common FFI setup.
|
|
261
|
+
#
|
|
262
|
+
# @param encrypted [Bytes] Encrypted data to decrypt.
|
|
263
|
+
# @yield [input, output] Block that performs the actual decryption call.
|
|
264
|
+
#
|
|
265
|
+
# @return [Bytes] Decrypted plain data.
|
|
266
|
+
def perform_decryption(encrypted)
|
|
267
|
+
encrypted = encrypted.b if encrypted.respond_to?(:b)
|
|
268
|
+
|
|
269
|
+
encrypted_ptr = FFI::MemoryPointer.new(:char, encrypted.bytesize)
|
|
270
|
+
encrypted_ptr.put_bytes(0, encrypted)
|
|
271
|
+
|
|
272
|
+
input = Age::Bindings::AgeInput.new
|
|
273
|
+
input[:data] = encrypted_ptr
|
|
274
|
+
input[:length] = encrypted.bytesize
|
|
275
|
+
|
|
276
|
+
plain_ptr = FFI::MemoryPointer.new(:pointer)
|
|
277
|
+
plain_len_ptr = FFI::MemoryPointer.new(:int)
|
|
278
|
+
|
|
279
|
+
output = Age::Bindings::AgeOutput.new
|
|
280
|
+
output[:data] = plain_ptr
|
|
281
|
+
output[:length] = plain_len_ptr
|
|
282
|
+
|
|
283
|
+
err_ptr = yield(input, output)
|
|
284
|
+
unless err_ptr.null?
|
|
285
|
+
err_msg = read_string_from_pointer(err_ptr)
|
|
286
|
+
raise DecryptionError, err_msg
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
bytes = read_bytes_from_pointer(plain_ptr.read_pointer, plain_len_ptr.read_int)
|
|
290
|
+
bytes.force_encoding('BINARY')
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
##
|
|
294
|
+
# Performs file encryption with common file handling.
|
|
295
|
+
#
|
|
296
|
+
# @param infile [String] Input file path.
|
|
297
|
+
# @param outfile [String, nil] Output file path.
|
|
298
|
+
# @yield [plain] Block that performs the actual encryption.
|
|
299
|
+
#
|
|
300
|
+
# @return [void]
|
|
301
|
+
def perform_file_encryption(infile, outfile)
|
|
302
|
+
outfile ||= "#{infile}.age"
|
|
303
|
+
plain = File.binread(infile)
|
|
304
|
+
encrypted = yield(plain)
|
|
305
|
+
File.binwrite(outfile, encrypted)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
##
|
|
309
|
+
# Performs file decryption with common file handling and armor detection.
|
|
310
|
+
#
|
|
311
|
+
# @param infile [String] Input file path.
|
|
312
|
+
# @param outfile [String, nil] Output file path.
|
|
313
|
+
# @yield [encrypted, armor] Block that performs the actual decryption.
|
|
314
|
+
#
|
|
315
|
+
# @return [void]
|
|
316
|
+
def perform_file_decryption(infile, outfile)
|
|
317
|
+
outfile ||= File.basename(infile, '.*')
|
|
318
|
+
encrypted = File.binread(infile)
|
|
319
|
+
armor = detect_armor_format?(encrypted)
|
|
320
|
+
plain = yield(encrypted, armor)
|
|
321
|
+
File.binwrite(outfile, plain)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
##
|
|
325
|
+
# Detects if the encrypted data is in armor format.
|
|
326
|
+
#
|
|
327
|
+
# @param encrypted [String] Encrypted data.
|
|
328
|
+
#
|
|
329
|
+
# @return [Boolean] True if armored, false otherwise.
|
|
330
|
+
def detect_armor_format?(encrypted)
|
|
331
|
+
encrypted.start_with?('-----BEGIN AGE ENCRYPTED FILE-----') &&
|
|
332
|
+
encrypted.end_with?("-----END AGE ENCRYPTED FILE-----\n")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
##
|
|
336
|
+
# Reads a string from a pointer and frees the memory.
|
|
337
|
+
#
|
|
338
|
+
# @param ptr [FFI::Pointer] Pointer to the string.
|
|
339
|
+
#
|
|
340
|
+
# @return [String, nil] The string read from the pointer, or nil.
|
|
341
|
+
def read_string_from_pointer(ptr)
|
|
342
|
+
return nil if ptr.null?
|
|
343
|
+
|
|
344
|
+
str = ptr.read_string
|
|
345
|
+
Age::Bindings.free_memory(ptr)
|
|
346
|
+
|
|
347
|
+
str
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
##
|
|
351
|
+
# Reads bytes from a pointer and frees the memory.
|
|
352
|
+
#
|
|
353
|
+
# @param ptr [FFI::Pointer] Pointer to the bytes.
|
|
354
|
+
# @param length [Integer] Length of the bytes to read.
|
|
355
|
+
#
|
|
356
|
+
# @return [String, nil] The bytes read from the pointer, or nil.
|
|
357
|
+
def read_bytes_from_pointer(ptr, length)
|
|
358
|
+
return nil if ptr.null? || length.zero?
|
|
359
|
+
|
|
360
|
+
bytes = ptr.read_bytes(length)
|
|
361
|
+
Age::Bindings.free_memory(ptr)
|
|
362
|
+
|
|
363
|
+
bytes
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
private :perform_encryption, :perform_decryption, :perform_file_encryption,
|
|
367
|
+
:perform_file_decryption, :detect_armor_format?,
|
|
368
|
+
:read_string_from_pointer, :read_bytes_from_pointer
|
|
369
|
+
end
|
|
370
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: age.rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tobias Schäfer
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.17'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.17'
|
|
26
|
+
description: 'age.rb: Ruby bindings for age
|
|
27
|
+
|
|
28
|
+
'
|
|
29
|
+
email:
|
|
30
|
+
- github@blackox.org
|
|
31
|
+
executables: []
|
|
32
|
+
extensions:
|
|
33
|
+
- ext/extconf.rb
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- ext/Makefile
|
|
37
|
+
- ext/age.go
|
|
38
|
+
- ext/extconf.rb
|
|
39
|
+
- ext/go.mod
|
|
40
|
+
- ext/go.sum
|
|
41
|
+
- lib/age.rb
|
|
42
|
+
- lib/age/bindings.rb
|
|
43
|
+
- lib/age/errors.rb
|
|
44
|
+
- lib/age/version.rb
|
|
45
|
+
homepage: https://github.com/tschaefer/age.rb
|
|
46
|
+
licenses:
|
|
47
|
+
- BSD-3-Clause
|
|
48
|
+
metadata:
|
|
49
|
+
rubygems_mfa_required: 'true'
|
|
50
|
+
source_code_uri: https://github.com/tschaefer/age.rb
|
|
51
|
+
bug_tracker_uri: https://github.com/tschaefer/age.rb/issues
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 3.2.3
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 4.0.3
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: 'age.rb: Ruby bindings for age'
|
|
69
|
+
test_files: []
|