This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / tools / pass / age-plugin-se-dummy.py
6.1 kB 171 lines
1#!/usr/bin/env python3 2""" 3Dummy age-plugin-se for Linux. 4 5This plugin allows encrypting secrets to Secure Enclave recipients (age1se... addresses) 6from Linux machines. The encrypted files can then be decrypted on macOS using the real 7age-plugin-se with hardware Secure Enclave support. 8 9The crypto is identical to the real plugin - only the key storage differs: 10- Real plugin: Private keys stored in Apple Secure Enclave hardware 11- This plugin: Only supports encryption (no private key storage) 12""" 13 14import sys 15import io 16import base64 17import hashlib 18from cryptography.hazmat.primitives.asymmetric import ec 19from cryptography.hazmat.primitives import hashes, serialization 20from cryptography.hazmat.primitives.kdf.hkdf import HKDF 21from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 22 23 24def bech32_decode(s): 25 """Simple bech32 decode for age1se addresses.""" 26 charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 27 hrp_end = s.rfind('1') 28 if hrp_end < 1: 29 return None 30 data = s[hrp_end + 1:] 31 decoded = [] 32 for c in data: 33 if c not in charset: 34 return None 35 decoded.append(charset.index(c)) 36 # Convert from 5-bit to 8-bit groups 37 bits = 0 38 value = 0 39 result = [] 40 for d in decoded[:-6]: # Skip checksum 41 value = (value << 5) | d 42 bits += 5 43 if bits >= 8: 44 bits -= 8 45 result.append((value >> bits) & 0xff) 46 value &= (1 << bits) - 1 47 return bytes(result) 48 49 50def write_stdout(s): 51 """Write to stdout with explicit flush (unbuffered).""" 52 stdout_raw.write(s.encode('utf-8')) 53 stdout_raw.flush() 54 55 56def handle_recipient_v1(): 57 """Handle the age plugin recipient-v1 protocol.""" 58 file_keys = [] 59 recipients = [] 60 got_done = False 61 62 # Read commands until -> done 63 while True: 64 line = sys.stdin.readline() 65 if not line: # EOF 66 break 67 line = line.strip() 68 69 if line.startswith("-> add-recipient "): 70 recipients.append(line[17:]) 71 elif line == "-> wrap-file-key": 72 # Read body lines until empty line OR next command 73 body_parts = [] 74 while True: 75 body_line = sys.stdin.readline() 76 if not body_line: 77 break 78 body_line_stripped = body_line.strip() 79 if body_line_stripped == "": 80 break # Empty line terminates body 81 if body_line_stripped.startswith("->"): 82 # Next command encountered 83 if body_line_stripped == "-> done": 84 got_done = True 85 break 86 body_parts.append(body_line_stripped) 87 file_keys.append("".join(body_parts)) 88 if got_done: 89 break 90 elif line == "-> done": 91 got_done = True 92 break 93 # Ignore other lines (grease commands, extension-labels, etc.) 94 95 if not got_done: 96 sys.exit(1) 97 98 # Process each file key for each recipient 99 for file_idx, file_key_b64 in enumerate(file_keys): 100 for recipient in recipients: 101 try: 102 # Decode recipient public key (compressed P-256, 33 bytes) 103 pubkey_bytes = bech32_decode(recipient) 104 if not pubkey_bytes: 105 raise ValueError("Failed to decode recipient") 106 107 # Generate tag from public key hash (first 4 bytes of SHA256) 108 tag = base64.b64encode( 109 hashlib.sha256(pubkey_bytes).digest()[:4] 110 ).decode().rstrip('=') 111 112 # Decode file key (add padding for base64) 113 padding = (4 - len(file_key_b64) % 4) % 4 114 file_key = base64.b64decode(file_key_b64 + '=' * padding) 115 116 # Load P-256 public key 117 recipient_pubkey = ec.EllipticCurvePublicKey.from_encoded_point( 118 ec.SECP256R1(), pubkey_bytes 119 ) 120 121 # Generate ephemeral key pair 122 ephemeral_private = ec.generate_private_key(ec.SECP256R1()) 123 ephemeral_public = ephemeral_private.public_key() 124 ephemeral_bytes = ephemeral_public.public_bytes( 125 serialization.Encoding.X962, 126 serialization.PublicFormat.CompressedPoint 127 ) 128 129 # ECDH shared secret 130 shared = ephemeral_private.exchange(ec.ECDH(), recipient_pubkey) 131 132 # HKDF to derive wrapping key (matches age-plugin-se format) 133 # salt = ephemeral || recipient_pubkey, info = "piv-p256" 134 hkdf_salt = ephemeral_bytes + pubkey_bytes 135 wrapping_key = HKDF( 136 algorithm=hashes.SHA256(), 137 length=32, 138 salt=hkdf_salt, 139 info=b"piv-p256", 140 ).derive(shared) 141 142 # Encrypt with ChaCha20-Poly1305 (zero nonce, no AAD) 143 cipher = ChaCha20Poly1305(wrapping_key) 144 encrypted = cipher.encrypt(b'\x00' * 12, file_key, None) 145 146 # Format output (base64 without padding) 147 ephemeral_b64 = base64.b64encode(ephemeral_bytes).decode().rstrip('=') 148 encrypted_b64 = base64.b64encode(encrypted).decode().rstrip('=') 149 150 # Output stanza (header + body) 151 header = f"-> recipient-stanza {file_idx} piv-p256 {tag} {ephemeral_b64}" 152 write_stdout(header + '\n') 153 write_stdout(encrypted_b64 + '\n') 154 155 except Exception: 156 pass # Silently skip failed recipients 157 158 # Signal completion with empty line after done (required by plugin protocol) 159 write_stdout("-> done\n\n") 160 sys.exit(0) 161 162 163if __name__ == "__main__": 164 # Use unbuffered stdout for reliable plugin communication 165 stdout_raw = io.FileIO(sys.stdout.fileno(), mode='w', closefd=False) 166 167 if len(sys.argv) > 1 and sys.argv[1] == '--age-plugin=recipient-v1': 168 handle_recipient_v1() 169 else: 170 # Unknown mode 171 sys.exit(1)