mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-09 18:57:22 +02:00
Compare commits
2 Commits
update_loc
...
extensions
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f1f83113 | ||
|
|
3e128d2a81 |
@@ -18,7 +18,7 @@ Open [Sources/Secretive.xcodeproj](Sources/Secretive.xcodeproj) in Xcode.
|
||||
|
||||
### Translate
|
||||
|
||||
Navigate to [Sources/Packages/Localizable.xcstrings](Sources/Packages/Localizable.xcstrings).
|
||||
Navigate to [Secretive/Localizable](Sources/Secretive/Localizable.xcstrings).
|
||||
|
||||
<img src="/.github/readme/localize_sidebar.png" alt="Screenshot of Xcode navigating to the Localizable file" width="300">
|
||||
|
||||
@@ -32,12 +32,6 @@ Start translating! You'll see a list of english phrases, and a space to add a tr
|
||||
|
||||
Push your changes and open a pull request.
|
||||
|
||||
### Handling Updates
|
||||
|
||||
When your translation is merged, I'll invite you to the [secretive-localizers](https://github.com/secretive-localizers) group. I'll tag this group anytime there's a new set of strings, in the hopes that you'll update the translation. If you don't want to be notified, feel free to decline the invitation or leave the organization at any time.
|
||||
|
||||
### Questions
|
||||
|
||||
Please open an issue if you have a question about translating the app. I'm more than happy to clarify any terms that are ambiguous or confusing. Thanks for contributing!
|
||||
|
||||
|
||||
|
||||
@@ -61,4 +61,4 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
|
||||
|
||||
## Security
|
||||
|
||||
Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)
|
||||
If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY."
|
||||
|
||||
@@ -24,4 +24,4 @@ The latest version on the [Releases page](https://github.com/maxgoedjen/secretiv
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)
|
||||
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY."
|
||||
|
||||
@@ -43,7 +43,7 @@ extension Agent {
|
||||
}
|
||||
let requestTypeInt = data[4]
|
||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription) for unknown request type \(requestTypeInt)")
|
||||
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
|
||||
}
|
||||
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
||||
@@ -66,10 +66,25 @@ extension Agent {
|
||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||
response.append(try await sign(data: data, provenance: provenance))
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
||||
case .protocolExtension:
|
||||
response.append(SSHAgent.ResponseType.agentExtensionResponse.data)
|
||||
try await handleExtension(data)
|
||||
default:
|
||||
let reader = OpenSSHReader(data: data)
|
||||
while true {
|
||||
do {
|
||||
let payloadHash = try reader.readNextChunk()
|
||||
print(String(String(decoding: payloadHash, as: UTF8.self)))
|
||||
print(payloadHash)
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.debug("Agent received valid request of type \(requestType.debugDescription), but not currently supported.")
|
||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||
}
|
||||
} catch {
|
||||
response.removeAll()
|
||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||
response = SSHAgent.ResponseType.agentFailure.data
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||
}
|
||||
return response.lengthAndData
|
||||
@@ -77,6 +92,28 @@ extension Agent {
|
||||
|
||||
}
|
||||
|
||||
// PROTOCOL EXTENSIONS
|
||||
extension Agent {
|
||||
|
||||
func handleExtension(_ data: Data) async throws {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
guard try reader.readNextChunkAsString() == "session-bind@openssh.com" else { throw UnsupportedExtensionError() }
|
||||
let hostKey = try reader.readNextChunk()
|
||||
let keyReader = OpenSSHReader(data: hostKey)
|
||||
_ = try keyReader.readNextChunkAsString() // Key Type
|
||||
let keyData = try keyReader.readNextChunk()
|
||||
let sessionID = try reader.readNextChunk()
|
||||
let signatureData = try reader.readNextChunk()
|
||||
let forwarding = try reader.readNextBytes(as: Bool.self)
|
||||
let signatureReader = OpenSSHSignatureReader()
|
||||
guard try signatureReader.verify(signatureData, for: sessionID, with: keyData) else { throw SignatureVerificationFailedError() }
|
||||
print("Fowarding: \(forwarding)")
|
||||
}
|
||||
|
||||
struct UnsupportedExtensionError: Error {}
|
||||
struct SignatureVerificationFailedError: Error {}
|
||||
}
|
||||
|
||||
extension Agent {
|
||||
|
||||
/// Lists the identities available for signing operations
|
||||
@@ -112,7 +149,7 @@ extension Agent {
|
||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
let payloadHash = reader.readNextChunk()
|
||||
let payloadHash = try reader.readNextChunk()
|
||||
let hash: Data
|
||||
|
||||
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
||||
@@ -129,7 +166,7 @@ extension Agent {
|
||||
|
||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||
|
||||
let dataToSign = reader.readNextChunk()
|
||||
let dataToSign = try reader.readNextChunk()
|
||||
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
|
||||
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||
|
||||
|
||||
@@ -10,13 +10,32 @@ extension SSHAgent {
|
||||
|
||||
case requestIdentities = 11
|
||||
case signRequest = 13
|
||||
case addIdentity = 17
|
||||
case removeIdentity = 18
|
||||
case removeAllIdentities = 19
|
||||
case addIDConstrained = 25
|
||||
case addSmartcardKey = 20
|
||||
case removeSmartcardKey = 21
|
||||
case lock = 22
|
||||
case unlock = 23
|
||||
case addSmartcardKeyConstrained = 26
|
||||
case protocolExtension = 27
|
||||
|
||||
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .requestIdentities:
|
||||
return "RequestIdentities"
|
||||
case .signRequest:
|
||||
return "SignRequest"
|
||||
case .requestIdentities: "SSH_AGENTC_REQUEST_IDENTITIES"
|
||||
case .signRequest: "SSH_AGENTC_SIGN_REQUEST"
|
||||
case .addIdentity: "SSH_AGENTC_ADD_IDENTITY"
|
||||
case .removeIdentity: "SSH_AGENTC_REMOVE_IDENTITY"
|
||||
case .removeAllIdentities: "SSH_AGENTC_REMOVE_ALL_IDENTITIES"
|
||||
case .addIDConstrained: "SSH_AGENTC_ADD_ID_CONSTRAINED"
|
||||
case .addSmartcardKey: "SSH_AGENTC_ADD_SMARTCARD_KEY"
|
||||
case .removeSmartcardKey: "SSH_AGENTC_REMOVE_SMARTCARD_KEY"
|
||||
case .lock: "SSH_AGENTC_LOCK"
|
||||
case .unlock: "SSH_AGENTC_UNLOCK"
|
||||
case .addSmartcardKeyConstrained: "SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED"
|
||||
case .protocolExtension: "SSH_AGENTC_EXTENSION"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,17 +47,17 @@ extension SSHAgent {
|
||||
case agentSuccess = 6
|
||||
case agentIdentitiesAnswer = 12
|
||||
case agentSignResponse = 14
|
||||
case agentExtensionFailure = 28
|
||||
case agentExtensionResponse = 29
|
||||
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .agentFailure:
|
||||
return "AgentFailure"
|
||||
case .agentSuccess:
|
||||
return "AgentSuccess"
|
||||
case .agentIdentitiesAnswer:
|
||||
return "AgentIdentitiesAnswer"
|
||||
case .agentSignResponse:
|
||||
return "AgentSignResponse"
|
||||
case .agentFailure: "SSH_AGENT_FAILURE"
|
||||
case .agentSuccess: "SSH_AGENT_SUCCESS"
|
||||
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
|
||||
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
|
||||
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
|
||||
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,20 +30,24 @@ public actor OpenSSHCertificateHandler: Sendable {
|
||||
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
||||
public func publicKeyHash(from hash: Data) -> Data? {
|
||||
let reader = OpenSSHReader(data: hash)
|
||||
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
|
||||
switch certType {
|
||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||
_ = reader.readNextChunk() // nonce
|
||||
let curveIdentifier = reader.readNextChunk()
|
||||
let publicKey = reader.readNextChunk()
|
||||
do {
|
||||
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
|
||||
switch certType {
|
||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||
_ = try reader.readNextChunk() // nonce
|
||||
let curveIdentifier = try reader.readNextChunk()
|
||||
let publicKey = try reader.readNextChunk()
|
||||
|
||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ extension OpenSSHPublicKeyWriter {
|
||||
|
||||
extension OpenSSHPublicKeyWriter {
|
||||
|
||||
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
||||
func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
||||
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
|
||||
// Keychain stores it as a thin ASN.1 wrapper with this format:
|
||||
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
|
||||
|
||||
@@ -13,7 +13,8 @@ public final class OpenSSHReader {
|
||||
|
||||
/// Reads the next chunk of data from the playload.
|
||||
/// - Returns: The next chunk of data.
|
||||
public func readNextChunk() -> Data {
|
||||
public func readNextChunk() throws -> Data {
|
||||
guard remaining.count > UInt32.bitWidth/8 else { throw EndOfData() }
|
||||
let lengthRange = 0..<(UInt32.bitWidth/8)
|
||||
let lengthChunk = remaining[lengthRange]
|
||||
remaining.removeSubrange(lengthRange)
|
||||
@@ -25,4 +26,18 @@ public final class OpenSSHReader {
|
||||
return ret
|
||||
}
|
||||
|
||||
public func readNextBytes<T>(as: T.Type) throws -> T {
|
||||
let lengthRange = 0..<MemoryLayout<T>.size
|
||||
let lengthChunk = remaining[lengthRange]
|
||||
remaining.removeSubrange(lengthRange)
|
||||
return lengthChunk.bytes.unsafeLoad(as: T.self)
|
||||
}
|
||||
|
||||
|
||||
public func readNextChunkAsString() throws -> String {
|
||||
try String(decoding: readNextChunk(), as: UTF8.self)
|
||||
}
|
||||
|
||||
public struct EndOfData: Error {}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Security
|
||||
|
||||
/// Reads OpenSSH representations of Secrets.
|
||||
public struct OpenSSHSignatureReader: Sendable {
|
||||
|
||||
/// Initializes the reader.
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func verify(_ signatureData: Data, for signedData: Data, with publicKey: Data) throws -> Bool {
|
||||
let reader = OpenSSHReader(data: signatureData)
|
||||
let signatureType = try reader.readNextChunkAsString()
|
||||
let signatureData = try reader.readNextChunk()
|
||||
switch signatureType {
|
||||
case "ssh-rsa":
|
||||
let attributes = KeychainDictionary([
|
||||
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
||||
kSecAttrKeySizeInBits: 2048,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
||||
])
|
||||
var verifyError: SecurityError?
|
||||
let untyped: CFTypeRef? = SecKeyCreateWithData(publicKey as CFData, attributes, &verifyError)
|
||||
guard let untypedSafe = untyped else {
|
||||
throw KeychainError(statusCode: errSecSuccess)
|
||||
}
|
||||
let key = untypedSafe as! SecKey
|
||||
return SecKeyVerifySignature(key, .rsaSignatureMessagePKCS1v15SHA512, signedData as CFData, signatureData as CFData, nil)
|
||||
case "ecdsa-sha2-nistp256":
|
||||
return try P256.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
|
||||
case "ecdsa-sha2-nistp384":
|
||||
return try P384.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
|
||||
case "ecdsa-sha2-nistp521":
|
||||
return try P521.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
|
||||
case "ssh-ed25519":
|
||||
return try Curve25519.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
|
||||
case "ssh-mldsa-65":
|
||||
if #available(macOS 26.0, *) {
|
||||
return try MLDSA65.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
|
||||
} else {
|
||||
throw UnsupportedSignatureType()
|
||||
}
|
||||
case "ssh-mldsa-87":
|
||||
if #available(macOS 26.0, *) {
|
||||
return try MLDSA87.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
|
||||
} else {
|
||||
throw UnsupportedSignatureType()
|
||||
}
|
||||
default:
|
||||
throw UnsupportedSignatureType()
|
||||
}
|
||||
}
|
||||
|
||||
public struct UnsupportedSignatureType: Error {}
|
||||
|
||||
}
|
||||
@@ -112,7 +112,7 @@ extension SecureEnclave {
|
||||
var accessError: SecurityError?
|
||||
let flags: SecAccessControlCreateFlags = switch attributes.authentication {
|
||||
case .notRequired:
|
||||
[.privateKeyUsage]
|
||||
[]
|
||||
case .presenceRequired:
|
||||
[.userPresence, .privateKeyUsage]
|
||||
case .biometryCurrent:
|
||||
|
||||
Reference in New Issue
Block a user