Skip to main content

bee/swarm/
keys.rs

1//! `PrivateKey` / `PublicKey` and the Ethereum signed-message signer.
2//!
3//! Mirrors bee-go's `pkg/swarm/typed_bytes.go` crypto block. Uses
4//! pure-Rust [`k256`] (RustCrypto secp256k1) so we don't pull in a
5//! libsecp C dependency.
6//!
7//! The scheme matches bee-js:
8//!
9//! ```text
10//! digest = keccak256("\x19Ethereum Signed Message:\n32" || keccak256(data))
11//! ```
12//!
13//! and signatures are stored with `V ∈ {27, 28}` on the wire (v0
14//! / v1 are normalized on the way out, denormalized on the way in).
15
16use std::fmt;
17use std::str::FromStr;
18
19use k256::ecdsa::{
20    RecoveryId, Signature as K256Signature, SigningKey, VerifyingKey,
21    signature::hazmat::PrehashSigner,
22};
23use sha3::{Digest, Keccak256};
24
25use crate::swarm::bytes::{decode_hex, encode_hex};
26use crate::swarm::errors::Error;
27use crate::swarm::typed_bytes::{
28    ETH_ADDRESS_LENGTH, EthAddress, PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH,
29    Signature,
30};
31
32// ---- PrivateKey --------------------------------------------------------
33
34/// secp256k1 private key (32 bytes).
35///
36/// Not [`Copy`] — copies of secret material should be intentional.
37#[derive(Clone, PartialEq, Eq)]
38pub struct PrivateKey([u8; PRIVATE_KEY_LENGTH]);
39
40impl PrivateKey {
41    /// Length in bytes.
42    pub const LENGTH: usize = PRIVATE_KEY_LENGTH;
43
44    /// Construct from raw bytes.
45    pub fn new(b: &[u8]) -> Result<Self, Error> {
46        if b.len() != PRIVATE_KEY_LENGTH {
47            return Err(Error::LengthMismatch {
48                kind: "PrivateKey",
49                expected: &[PRIVATE_KEY_LENGTH],
50                got: b.len(),
51            });
52        }
53        let mut a = [0u8; PRIVATE_KEY_LENGTH];
54        a.copy_from_slice(b);
55        Ok(Self(a))
56    }
57
58    /// Parse from hex (with or without `0x` prefix).
59    pub fn from_hex(s: &str) -> Result<Self, Error> {
60        Self::new(&decode_hex(s)?)
61    }
62
63    /// Borrow the raw bytes.
64    pub fn as_bytes(&self) -> &[u8] {
65        &self.0
66    }
67
68    /// Lowercase hex, no `0x` prefix.
69    pub fn to_hex(&self) -> String {
70        encode_hex(&self.0)
71    }
72
73    fn signing_key(&self) -> Result<SigningKey, Error> {
74        SigningKey::from_slice(&self.0).map_err(Error::crypto)
75    }
76
77    /// Derive the uncompressed (64-byte `X || Y`) public key.
78    pub fn public_key(&self) -> Result<PublicKey, Error> {
79        let sk = self.signing_key()?;
80        let vk = VerifyingKey::from(&sk);
81        let point = vk.to_encoded_point(false);
82        // SEC1 uncompressed: 0x04 || X(32) || Y(32). Strip the prefix.
83        let bytes = point.as_bytes();
84        debug_assert_eq!(bytes.len(), 65);
85        let mut a = [0u8; PUBLIC_KEY_LENGTH];
86        a.copy_from_slice(&bytes[1..]);
87        Ok(PublicKey(a))
88    }
89
90    /// Sign `data` using the Ethereum signed-message scheme and return
91    /// a 65-byte `R || S || V` signature with `V ∈ {27, 28}`.
92    pub fn sign(&self, data: &[u8]) -> Result<Signature, Error> {
93        let digest = eth_signed_message_digest(data);
94        let sk = self.signing_key()?;
95        let (sig, recovery_id): (K256Signature, RecoveryId) =
96            sk.sign_prehash(&digest).map_err(Error::crypto)?;
97        let normalized = sig.normalize_s().unwrap_or(sig);
98        // k256 returns the recovery id corresponding to the original sig;
99        // when normalize_s flips s, the recovery id also flips parity.
100        let recovery_id = if normalized != sig {
101            RecoveryId::from_byte(recovery_id.to_byte() ^ 1).expect("flipped recovery id valid")
102        } else {
103            recovery_id
104        };
105
106        let mut out = [0u8; SIGNATURE_LENGTH];
107        let bytes = normalized.to_bytes();
108        out[..64].copy_from_slice(&bytes);
109        out[64] = recovery_id.to_byte() + 27;
110        Signature::new(&out)
111    }
112}
113
114impl fmt::Debug for PrivateKey {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        // Don't leak private bytes in Debug output.
117        f.write_str("PrivateKey(<redacted>)")
118    }
119}
120
121impl FromStr for PrivateKey {
122    type Err = Error;
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        Self::from_hex(s)
125    }
126}
127
128// ---- PublicKey ---------------------------------------------------------
129
130/// Uncompressed secp256k1 public key (`X || Y`, 64 bytes).
131///
132/// Constructors accept the 33-byte SEC1-compressed encoding too.
133#[derive(Clone, Copy, PartialEq, Eq, Hash)]
134pub struct PublicKey([u8; PUBLIC_KEY_LENGTH]);
135
136impl PublicKey {
137    /// Length in bytes when stored uncompressed.
138    pub const LENGTH: usize = PUBLIC_KEY_LENGTH;
139
140    /// Construct from bytes. Accepts 64-byte uncompressed (`X || Y`)
141    /// or 33-byte compressed (SEC1).
142    pub fn new(b: &[u8]) -> Result<Self, Error> {
143        match b.len() {
144            PUBLIC_KEY_LENGTH => {
145                let mut a = [0u8; PUBLIC_KEY_LENGTH];
146                a.copy_from_slice(b);
147                Ok(Self(a))
148            }
149            33 => {
150                let vk = VerifyingKey::from_sec1_bytes(b).map_err(Error::crypto)?;
151                let point = vk.to_encoded_point(false);
152                let bytes = point.as_bytes();
153                let mut a = [0u8; PUBLIC_KEY_LENGTH];
154                a.copy_from_slice(&bytes[1..]);
155                Ok(Self(a))
156            }
157            n => Err(Error::LengthMismatch {
158                kind: "PublicKey",
159                expected: &[33, PUBLIC_KEY_LENGTH],
160                got: n,
161            }),
162        }
163    }
164
165    /// Parse from hex (with or without `0x` prefix). Length must be 33
166    /// (compressed) or 64 (uncompressed).
167    pub fn from_hex(s: &str) -> Result<Self, Error> {
168        Self::new(&decode_hex(s)?)
169    }
170
171    /// Borrow the raw 64-byte uncompressed encoding.
172    pub fn as_bytes(&self) -> &[u8] {
173        &self.0
174    }
175
176    /// Lowercase hex of the uncompressed encoding, no `0x` prefix.
177    pub fn to_hex(&self) -> String {
178        encode_hex(&self.0)
179    }
180
181    /// Ethereum address: last 20 bytes of `keccak256(X || Y)`.
182    pub fn address(&self) -> EthAddress {
183        let mut h = Keccak256::new();
184        h.update(self.0);
185        let out = h.finalize();
186        let mut a = [0u8; ETH_ADDRESS_LENGTH];
187        a.copy_from_slice(&out[12..]);
188        EthAddress::new(&a).expect("hash slice has fixed length")
189    }
190
191    /// 33-byte SEC1-compressed encoding.
192    pub fn compressed_bytes(&self) -> Result<[u8; 33], Error> {
193        let mut full = [0u8; 65];
194        full[0] = 0x04;
195        full[1..].copy_from_slice(&self.0);
196        let vk = VerifyingKey::from_sec1_bytes(&full).map_err(Error::crypto)?;
197        let point = vk.to_encoded_point(true);
198        let bytes = point.as_bytes();
199        if bytes.len() != 33 {
200            return Err(Error::crypto("compressed point not 33 bytes"));
201        }
202        let mut a = [0u8; 33];
203        a.copy_from_slice(bytes);
204        Ok(a)
205    }
206
207    /// Lowercase hex of the compressed encoding, no `0x` prefix.
208    pub fn compressed_hex(&self) -> Result<String, Error> {
209        Ok(encode_hex(&self.compressed_bytes()?))
210    }
211}
212
213impl fmt::Debug for PublicKey {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        write!(f, "PublicKey({})", self.to_hex())
216    }
217}
218
219impl fmt::Display for PublicKey {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        f.write_str(&self.to_hex())
222    }
223}
224
225impl FromStr for PublicKey {
226    type Err = Error;
227    fn from_str(s: &str) -> Result<Self, Self::Err> {
228        Self::from_hex(s)
229    }
230}
231
232// ---- Signature recovery ------------------------------------------------
233
234impl Signature {
235    /// Recover the public key that produced this signature for `data`,
236    /// using the Ethereum signed-message scheme.
237    pub fn recover_public_key(&self, data: &[u8]) -> Result<PublicKey, Error> {
238        let digest = eth_signed_message_digest(data);
239        let bytes = self.as_bytes();
240        let v = bytes[64];
241        let recovery_byte = if v >= 27 { v - 27 } else { v };
242        let recovery_id =
243            RecoveryId::from_byte(recovery_byte).ok_or_else(|| Error::crypto("invalid V byte"))?;
244        let sig = K256Signature::from_slice(&bytes[..64]).map_err(Error::crypto)?;
245        let vk = VerifyingKey::recover_from_prehash(&digest, &sig, recovery_id)
246            .map_err(Error::crypto)?;
247        let point = vk.to_encoded_point(false);
248        let raw = point.as_bytes();
249        let mut a = [0u8; PUBLIC_KEY_LENGTH];
250        a.copy_from_slice(&raw[1..]);
251        Ok(PublicKey(a))
252    }
253
254    /// True iff the signature is valid against `data` and the recovered
255    /// signer matches `expected`.
256    pub fn is_valid(&self, data: &[u8], expected: EthAddress) -> bool {
257        match self.recover_public_key(data) {
258            Ok(pk) => pk.address() == expected,
259            Err(_) => false,
260        }
261    }
262}
263
264// ---- digest helper -----------------------------------------------------
265
266/// `keccak256("\x19Ethereum Signed Message:\n32" || keccak256(data))`.
267/// The exact digest Bee verifies SOC and feed signatures against.
268pub fn eth_signed_message_digest(data: &[u8]) -> [u8; 32] {
269    let mut h = Keccak256::new();
270    h.update(data);
271    let inner = h.finalize();
272
273    let mut h = Keccak256::new();
274    h.update(b"\x19Ethereum Signed Message:\n32");
275    h.update(inner);
276    let out = h.finalize();
277    let mut a = [0u8; 32];
278    a.copy_from_slice(&out);
279    a
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    fn priv_repeat(byte: u8) -> PrivateKey {
287        PrivateKey::new(&[byte; PRIVATE_KEY_LENGTH]).unwrap()
288    }
289
290    #[test]
291    fn private_to_public_to_address_is_consistent() {
292        let pk = priv_repeat(0x11);
293        let pub_a = pk.public_key().unwrap();
294        let pub_b = pk.public_key().unwrap();
295        assert_eq!(pub_a.as_bytes(), pub_b.as_bytes());
296        assert_eq!(pub_a.address(), pub_b.address());
297    }
298
299    #[test]
300    fn public_key_compressed_round_trip() {
301        let pk = priv_repeat(0x22);
302        let pub_a = pk.public_key().unwrap();
303        let compressed = pub_a.compressed_bytes().unwrap();
304        assert_eq!(compressed.len(), 33);
305        let pub_b = PublicKey::new(&compressed).unwrap();
306        assert_eq!(pub_a.as_bytes(), pub_b.as_bytes());
307    }
308
309    #[test]
310    fn sign_recover_round_trip() {
311        let pk = priv_repeat(0x33);
312        let data = b"hello swarm";
313        let sig = pk.sign(data).unwrap();
314        // V normalized to {27, 28} per bee-js wire format.
315        let v = sig.as_bytes()[64];
316        assert!(v == 27 || v == 28, "V was {v}");
317        let recovered = sig.recover_public_key(data).unwrap();
318        assert_eq!(recovered.as_bytes(), pk.public_key().unwrap().as_bytes());
319        assert!(sig.is_valid(data, pk.public_key().unwrap().address()));
320        assert!(!sig.is_valid(b"tampered", pk.public_key().unwrap().address()));
321    }
322
323    #[test]
324    fn debug_does_not_leak_private_bytes() {
325        let pk = priv_repeat(0x44);
326        let s = format!("{pk:?}");
327        assert!(!s.contains("44"));
328        assert!(s.contains("redacted"));
329    }
330}